├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Cask ├── LICENSE ├── Makefile ├── README.md ├── RelNotes ├── 0.1.6.org └── 0.2.org ├── images ├── ci-failure.png ├── ci-pending.png ├── ci-success.png ├── create.gif ├── pull-request.gif ├── scr1.png ├── scr2.png ├── scr3.png ├── scr4.png ├── scr5.png └── status.png ├── magithub-ci.el ├── magithub-comment.el ├── magithub-completion.el ├── magithub-core.el ├── magithub-dash.el ├── magithub-edit-mode.el ├── magithub-faces.el ├── magithub-issue-post.el ├── magithub-issue-tricks.el ├── magithub-issue-view.el ├── magithub-issue.el ├── magithub-label.el ├── magithub-notification.el ├── magithub-orgs.el ├── magithub-repo.el ├── magithub-settings.el ├── magithub-user.el ├── magithub.el ├── magithub.org ├── magithub.texi ├── screenshots.md └── test ├── magithub-test.el ├── mock-data └── get │ └── repos.d │ └── vermiculus.d │ └── magithub.81a9dfc7 └── test-helper.el /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Thanks for helping out! 2 | 3 | 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: 4 | 5 | # Feature Requests 6 | 7 | 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!) 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thanks for contributing! It's really appreciated :smile: 2 | 3 | Here are some things to remember: 4 | 5 | - 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. 6 | 7 | - If you're fixing a bug or implementing a new feature, add something to the appropriate `RelNotes/*.org` file. 8 | 9 | - If it's a breaking change (i.e., it could reasonably break someone's configuration), document that in the release notes as well. 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | *~ 3 | /magithub.html 4 | /magithub.pdf 5 | /magithub/ 6 | /build.log 7 | /magithub-autoloads.el 8 | /.emake/ 9 | /emake.mk 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | sudo: required 3 | dist: trusty 4 | cache: 5 | - directories: 6 | - "$HOME/emacs" 7 | matrix: 8 | fast_finish: true 9 | allow_failures: 10 | - env: EMACS_VERSION=snapshot 11 | - env: EMACS_VERSION=26.1 MELPA_STABLE=true 12 | env: 13 | global: 14 | - EMAKE_SHA1=1b23379eb5a9f82d3e2d227d0f217864e40f23e0 15 | matrix: 16 | - EMACS_VERSION=25.1 17 | - EMACS_VERSION=25.2 18 | - EMACS_VERSION=25.3 19 | - EMACS_VERSION=26.1 20 | - EMACS_VERSION=26.1 MELPA_STABLE=true 21 | - EMACS_VERSION=snapshot 22 | before_install: 23 | - wget "https://raw.githubusercontent.com/vermiculus/emake.el/${EMAKE_SHA1}/emake.mk" 24 | - make setup 25 | install: 26 | - make install 27 | script: 28 | - make --keep-going test 29 | notifications: 30 | email: 31 | on_success: never 32 | on_failure: change 33 | webhooks: 34 | urls: 35 | - https://webhooks.gitter.im/e/b1163bae60c65660fbd2 36 | on_success: change 37 | on_failure: always 38 | on_start: never 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thanks for contributing to this project. It's people like 4 | you -- your time and effort in reporting bugs, suggesting features, or 5 | submitting pull requests -- that make this project and so many others 6 | like it in the Emacs world a joy to work with. Stay awesome! 7 | 8 | ## Roadmap 9 | 10 | Magithub's vision is to become the bridge between the `git` VCS and 11 | GitHub social network. Not only do I want to replicate the standard 12 | functionality you would expect from a GitHub client, but I want to 13 | closely *integrate* Magit's workflows with GitHub's featureset to 14 | develop and optimize the broader experience of using `git` with other 15 | people. 16 | 17 | Magit itself may include such support in the future, though probably 18 | to a less-specialized extent. At present, Magithub is focusing on 19 | GitHub (although the lessons learned here could be applied to a 20 | Magitlab, for instance). 21 | 22 | ## Reporting Bugs 23 | 24 | Ugh, nasty bugs! Every software project has them (except 25 | [TeX, vπ][tex-bug]), and many of them are found only by users like 26 | you. As you write your issue, please follow the instructions the 27 | issue template provides. A stack trace helps tremendously! 28 | 29 | Sometimes there are intermittent bugs that cannot be reproduced 30 | easily. Anyone who develops software can tell you that it is very 31 | difficult to debug an issue that you cannot see. For this reason, the 32 | 'unconfirmed' label indicates an issue that hasn't been reproduced by 33 | a maintainer and the 'waiting' label indicates an issue is waiting for 34 | some response from the folks who are actually experiencing it. Any 35 | issue that has had the 'waiting' label for more than two weeks can be 36 | closed as 'not reproducible'. If you are still having the issue after 37 | that time, please do re-open the issue! I don't mean to say that bugs 38 | are features, but I don't want to give a false first impression of 39 | bugginess. 40 | 41 | ## Suggesting Features 42 | 43 | Feature requests are always welcome! Pull requests even more so. 44 | :wink: Know however that this is a project I do in my free time; 45 | sometimes life gets in the way of doing this development -- or even 46 | reviewing development from a pull request. Don't let that deter you 47 | :smile: It *will* be reviewed. 48 | 49 | ## Unit Tests 50 | 51 | Additions of more unit tests are always appreciated -- as well as 52 | improvements to the overall unit test approach. The *only* thing I 53 | would like to continue to avoid is making real API requests (since 54 | this makes pull requests difficult), so please mock the response for 55 | any such test you write. Reach out on Gitter if you need a hand. 56 | 57 | [tex-bug]: http://www.ntg.nl/maps/05/34.pdf 58 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package-file "magithub.el") 5 | 6 | (files "magithub*.el") 7 | 8 | (development 9 | (depends-on "cask") 10 | (depends-on "ert") 11 | (depends-on "ert-runner")) 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include config.mk 2 | 3 | PACKAGE_BASENAME := magithub 4 | EMAKE_SHA1 := 1b23379eb5a9f82d3e2d227d0f217864e40f23e0 5 | 6 | # override defaults 7 | ifeq ($(MELPA_STABLE),true) 8 | PACKAGE_ARCHIVES := gnu melpa-stable 9 | else 10 | PACKAGE_ARCHIVES := gnu melpa 11 | endif 12 | 13 | PACKAGE_TEST_ARCHIVES := gnu melpa 14 | 15 | include emake.mk 16 | 17 | .DEFAULT_GOAL: help 18 | 19 | ### Magithub targets 20 | 21 | .PHONY: clean install compile test 22 | 23 | clean: 24 | rm -rf $(EMAKE_WORKDIR) 25 | rm -rf *.elc 26 | 27 | emake.mk: ## download the emake Makefile 28 | wget 'https://raw.githubusercontent.com/vermiculus/emake.el/$(EMAKE_SHA1)/emake.mk' 29 | 30 | test: compile test-ert ## run tests 31 | lint: lint-package-lint lint-checkdoc ## run lints 32 | 33 | 34 | ### Manual-building 35 | 36 | ifndef ORG_LOAD_PATH 37 | ORG_LOAD_PATH = -L ../dash 38 | ORG_LOAD_PATH += -L ../org/lisp 39 | ORG_LOAD_PATH += -L ../org/contrib/lisp 40 | ORG_LOAD_PATH += -L ../ox-texinfo+ 41 | endif 42 | 43 | INSTALL_INFO ?= $(shell command -v ginstall-info || printf install-info) 44 | EMACS ?= emacs 45 | MAKEINFO ?= makeinfo 46 | MANUAL_HTML_ARGS ?= --css-ref /assets/the.css 47 | 48 | doc: info html html-dir pdf ## generate most manual formats 49 | info: $(PACKAGE_BASENAME).info dir ## generate info manual 50 | html: $(PACKAGE_BASENAME).html ## generate html manual file 51 | pdf: $(PACKAGE_BASENAME).pdf ## generate pdf manual 52 | 53 | ORG_ARGS = --batch -Q $(ORG_LOAD_PATH) -l ox-extra -l ox-texinfo+.el 54 | ORG_EVAL = --eval "(ox-extras-activate '(ignore-headlines))" 55 | ORG_EVAL += --eval "(setq indent-tabs-mode nil)" 56 | ORG_EVAL += --eval "(setq org-src-preserve-indentation nil)" 57 | ORG_EVAL += --funcall org-texinfo-export-to-texinfo 58 | 59 | # This target first bumps version strings in the Org source. The 60 | # necessary tools might be missing so other targets do not depend 61 | # on this target and it has to be run explicitly when appropriate. 62 | # 63 | # AMEND=t make texi Update manual to be amended to HEAD. 64 | # VERSION=N make texi Update manual for release. 65 | # 66 | .PHONY: texi 67 | texi: ## generate texi manual (see comments) 68 | @$(EMACS) $(ORG_ARGS) $(PACKAGE_BASENAME).org $(ORG_EVAL) 69 | @printf "\n" >> $(PACKAGE_BASENAME).texi 70 | @rm -f $(PACKAGE_BASENAME).texi~ 71 | 72 | %.info: %.texi 73 | @printf "Generating $@\n" 74 | @$(MAKEINFO) --no-split $< -o $@ 75 | 76 | dir: $(PACKAGE_BASENAME).info 77 | @printf "Generating $@\n" 78 | @printf "%s" $^ | xargs -n 1 $(INSTALL_INFO) --dir=$@ 79 | 80 | %.html: %.texi 81 | @printf "Generating $@\n" 82 | @$(MAKEINFO) --html --no-split $(MANUAL_HTML_ARGS) $< 83 | 84 | html-dir: $(PACKAGE_BASENAME).texi ## generate html manual directory 85 | @printf "Generating $(PACKAGE_BASENAME)/*.html\n" 86 | @$(MAKEINFO) --html $(MANUAL_HTML_ARGS) $< 87 | 88 | %.pdf: %.texi 89 | @printf "Generating $@\n" 90 | @texi2pdf --clean $< > /dev/null 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maintainer's Note 2 | 3 | I have not had as much time for extra-curriculars as I once had. 4 | At this point, I will never catch up to the obvious successor of 5 | this package, Forge, nor would I really want to :smile: 6 | 7 | Please check out that project instead! I believe there are things 8 | a GitHub-specific package could do that don't make sense to generalize, 9 | but Forge has served me well for the 95%. 10 | 11 | If you have a vision and would like to take over maintainership, 12 | reach out! 13 | 14 | --- 15 | 16 | Overview -- the status buffer 17 | 18 | # Magithub 19 | 20 | [![MELPA Status](http://melpa.milkbox.net/packages/magithub-badge.svg)](http://melpa.milkbox.net/#/magithub) 21 | [![Build Status](https://travis-ci.org/vermiculus/magithub.svg?branch=master)](https://travis-ci.org/vermiculus/magithub) 22 | [![Gitter](https://badges.gitter.im/vermiculus/magithub.svg)](https://gitter.im/vermiculus/magithub) 23 | [![MELPA Stable Status](http://melpa-stable.milkbox.net/packages/magithub-badge.svg)](http://melpa-stable.milkbox.net/#/magithub) 24 | [![GitHub Commits](https://img.shields.io/github/commits-since/vermiculus/magithub/latest.svg)](//github.com/vermiculus/magithub/releases) 25 | 26 | Magithub is a collection of interfaces to GitHub integrated into 27 | [Magit][magit] workflows: 28 | 29 | - Push new repositories 30 | - Fork existing ones 31 | - List and create issues and pull requests 32 | - Keep offline notes for your eyes only 33 | - Write comments 34 | - Manage labels and assignees 35 | - Stay up-to-date with status checks (e.g., CI) and notifications 36 | - ... 37 | 38 | as well as support for working offline. 39 | 40 | Happy hacking! 41 | 42 | ## Quick Start 43 | 44 | GitHub rate-limits unauthenticated requests heavily, so Magithub does 45 | not support making such requests. Consequently, `ghub` must be 46 | authenticated before using Magithub -- [see its README][ghub] for 47 | those instructions. 48 | 49 | ```elisp 50 | (use-package magithub 51 | :after magit 52 | :config 53 | (magithub-feature-autoinject t) 54 | (setq magithub-clone-default-directory "~/github")) 55 | ``` 56 | 57 | See [the full documentation][magithub-org] for more details. 58 | 59 | ## Getting Help 60 | 61 | See [the FAQ][magithub-org-faq] in the full documentation. If your 62 | question isn't answered there, [drop by the Gitter 63 | room]((https://gitter.im/vermiculus/magithub)). 64 | 65 | ## Support 66 | 67 | I'm gainfully and happily employed with a company that frowns on 68 | moonlighting, so unfortunately I can't accept any monetary support. 69 | Instead, [please direct any and all support to Magit 70 | itself][magit-donate]! 71 | 72 | ## Note 73 | 74 | There used to be another `magithub`: [nex3/magithub][old-magithub]. 75 | It's long-since unsupported and apparently has many issues 76 | (see [nex3/magithub#11][old-magithub-11] 77 | and [nex3/magithub#13][old-magithub-13]) and 78 | was [removed from MELPA][melpa-1126] some years ago. If you have it 79 | installed or configured, you may wish to remove/archive that 80 | configuration to avoid name-clash issues. Given that the package has 81 | been defunct for over three years and is likely abandoned, the present 82 | package's name will not be changing. 83 | 84 | [magit]: //www.github.com/magit/magit 85 | [magit-donate]: https://magit.vc/donate 86 | [ghub]: //github.com/tarsius/ghub 87 | [hub]: //hub.github.com 88 | [token]: https://github.com/settings/tokens 89 | [gh-use-package]: //github.com/jwiegley/use-package 90 | [old-magithub]: //github.com/nex3/magithub 91 | [old-magithub-11]: //github.com/nex3/magithub/issues/11 92 | [old-magithub-13]: //github.com/nex3/magithub/issues/13 93 | [melpa-1126]: //github.com/melpa/melpa/issues/1126 94 | [magithub-org]: https://github.com/vermiculus/magithub/blob/master/magithub.org 95 | [magithub-org-faq]: https://github.com/vermiculus/magithub/blob/master/magithub.org#faq 96 | -------------------------------------------------------------------------------- /RelNotes/0.1.6.org: -------------------------------------------------------------------------------- 1 | #+Title: Magithub Release 0.1.6 2 | #+Date: [2018-06-02 Sat] 3 | 4 | #+LINK: PR https://www.github.com/vermiculus/magithub/pull/%s 5 | #+LINK: BUG https://www.github.com/vermiculus/magithub/issues/%s 6 | 7 | * Breaking Changes 8 | - If you were using ~magit-header-line~ to customize the appearance of 9 | the =Issues= and =Pull Requests= section headers, those now use the 10 | ~magit-section-heading~ face. [[PR:196]] 11 | - Many functions related to issue/post creation have been reworked. 12 | Instead of the widget framework, we now use =magithub-edit-mode=. See 13 | more details in 'New Features'. [[PR:204]] 14 | - =magithub-dashboard-show-unread-notifications= is now called 15 | =magithub-dashboard-show-read-notifications= and all functionality 16 | pertaining to that variable has been updated. [[PR:251]] 17 | - Most settings, like the inclusion of sections in ~magit-status~, are 18 | now controlled by various =git config= properties. These settings are 19 | reachable under =H C=. The following functions/variables no longer 20 | exist: 21 | - ~magithub-ci-enabled-p~ (now ~magithub-settings-include-status-p~) 22 | - ~magithub-ci--set-enabled~ 23 | - ~magithub-ci-disable~ 24 | - ~magithub-ci-enable~ 25 | - ~magithub-toggle-ci-status-header~ 26 | - =magithub-cache= (now =magithub-settings-cache-behavior-override=; 27 | ~magithub-settings-cache-behavior~) 28 | - ~magithub-toggle-online~ 29 | - ~magithub-go-online~ 30 | - ~magithub-go-offline~ 31 | - ~magithub-source--remote~ 32 | - ~magithub--deftoggle~ 33 | - ~magithub-toggle-pull-requests~ 34 | - ~magithub-toggle-issues~ 35 | - ~magithub-proxy-set~ 36 | - ~magithub-proxy-set-default~ 37 | - ~magithub-enable~ 38 | - ~magithub-disable~ 39 | - ~magithub-enabled-toggle~ 40 | - =magithub-enabled-by-default= 41 | 42 | The various integration sections are now added to the appropriate 43 | hooks by ~magithub-feature-autoinject~ via =magithub-feature-list=. 44 | 45 | For more details on how to set configure Magithub now, consult the 46 | documentation inside ~magithub-settings-popup~ (=? KEY=) or read 47 | =magithub-settings.el=. [[PR:265]] 48 | - =hub.host= is no longer respected and has been replaced by user option 49 | ~magithub-github-hosts~. This most directly impacts GitHub Enterprise 50 | support. 51 | ** Caching [[PR:328]] 52 | Caching has been reworked mostly from the ground-up. 'Offline mode' 53 | is now manifest in a single, Boolean-valued git variable 54 | "magithub.online", which see ~magithub-settings--set-magithub.online~ 55 | for that behavior. 56 | 57 | - ~magithub-cache-invalidate~ was not used, so it is no longer 58 | available. 59 | - ~magithub-issue-refresh~ no longer takes parameters. 60 | 61 | * New Features 62 | - Browse commits by using =w= on a commit section. If the current 63 | section's value cannot be understood as a valid commit, use the 64 | =git-revision= at point. 65 | - ~magithub-feature-autoinject~ can now take a list of features to load. 66 | - Many symbols are now supported by ~thing-at-point~: 67 | - =github-user= 68 | - =github-issue= 69 | - =github-label= 70 | - =github-comment= 71 | - =github-repository= 72 | - =github-pull-request= 73 | - =github-notification= 74 | These symbols should allow other GitHub-sensitive packages to use 75 | the work Magithub has already done without depending on Magithub 76 | directly. [[PR:201]] 77 | - The widget interface for writing issues and pull requests is gone! 78 | Now, everything uses the framework debuted for writing comments. 79 | For issues and pull requests, the first line (i.e., everything up to 80 | the first newline character) is parsed as the title; everything else 81 | as the body. Now issues, pull requests, and comments use a common 82 | interface that supports submitting, canceling, and saving drafts to 83 | finish later. [[PR:204]] 84 | - You can now edit comments using =e= on a comment section. [[PR:206]] 85 | - When submitting pull requests of a single commit, the commit message 86 | is defaulted into the pull request body. Multiple commits? 87 | ~magit-log~ shows you the changes you want to merge. [[PR:239]] 88 | - Headers in issue-view mode are now easier to navigate. [[PR:250]] 89 | - Notifications are marked as read when visited in Emacs. [[PR:252]] 90 | - ~magithub-repo~ can now take a string of the form =user/repo=. This is 91 | helpful when writing other code that uses Magithub functionality. [[PR:253]] 92 | - New command ~magithub-pull-request-new-from-issue~ can create pull 93 | requests from issues. This creates a new pull request by copying 94 | the title/body from the source issue. (To be honest, this API 95 | endpoint is not what I thought it would be.) [[PR:256]] 96 | - Confirmation messages can now be skipped (or the default question 97 | behavior otherwise altered) using =git config= properties. See 98 | ~magithub-confirm-set-default-behavior~ or configure your settings 99 | locally (or globally) interactively when they're asked. [[PR:268]] 100 | [[PR:270]] 101 | - Use default branch of the repository as =BASE= if there's no upstream 102 | for the current branch. [[PR:269]] 103 | - Completion of issue numbers ("#123"), and user names ("@purcell") is 104 | supported in edit and commit message buffers via the standard 105 | ~completion-at-point~ mechanism, and therefore also via ~company~'s ~capf~ 106 | backend. This is enabled by default in certain buffers via the 107 | ~magithub-features~ mechanism. [[PR:263]], [[PR:278]] 108 | 109 | * Bug Fixes 110 | - ~magithub-clone~ is now asynchonous, and defers to the user whether 111 | or not to display the magit-process-buffer according to the value of 112 | ~magit-process-popup-time~. [[https://github.com/vermiculus/magithub/pull/340][PR:340]] 113 | - In ~magithub-repo~, an API request is no longer made when the 114 | repository context cannot be determined. 115 | - The list of labels is now correctly cached per-repository. [[PR:203]] 116 | - The full list of labels is now available for use when modifying 117 | issues and pull requests. [[PR:203]] 118 | - The cache (and other files in =magithub-dir=) are no longer added to 119 | the =recentf= list. [[PR:210]] 120 | - Consistently use ~magithub-request~. [[PR:229]] 121 | - ~magit-magithub-pull-request-section-map~ is now defined in terms of 122 | ~magit-magithub-issue-section-map~. [[PR:238]] 123 | - Fix autoloads to load and install the dispatch with Magit. [[PR:238]] 124 | - Remove awkward blank lines from the end of the dashboard. [[PR:238]] 125 | - Issue/PR drafts are deleted appropriately after successful 126 | submission. [[PR:247]] 127 | - Various performance improvements. [[PR:255]] 128 | - Ghub+ is now required in core. This should help users who utilize 129 | deferred loading. [[PR:260]] 130 | - Submitting pull requests to other repositories in some scenarios 131 | should now be fixed. [[PR:272]] 132 | - ~magithub-clone~ now correctly provides a default destination. [[PR:273]] 133 | - ~magithub-pull-request-new~ now uses a better check to test for pull 134 | requests of a single commit: [[PR:274]] 135 | #+BEGIN_SRC sh 136 | git rev-list --count BASE.. 137 | #+END_SRC 138 | - Authenticate correctly when marking notifications as read. [[PR:277]] 139 | - Don't call ~magit-get~ in a non-existent directory in ~magithub-clone~. 140 | [[PR:282]] 141 | - Pull requests now work in repositories with remotes that point to 142 | non-GitHub locations. [[PR:285]] 143 | - We now only prompt to refresh GitHub data when the command being run 144 | by the user is solely intended to refresh the buffer. [[PR:318]] 145 | - We no longer ever call =/rate_limit= directly, instead relying on an 146 | improved version of ~ghubp-ratelimit~ that handles GitHub Enterprise 147 | sanely. [[BUG:327]] 148 | -------------------------------------------------------------------------------- /RelNotes/0.2.org: -------------------------------------------------------------------------------- 1 | #+Title: Magithub Release 0.2 2 | #+Date: 3 | 4 | #+LINK: PR https://www.github.com/vermiculus/magithub/pull/%s 5 | #+LINK: BUG https://www.github.com/vermiculus/magithub/issues/%s 6 | 7 | * Bug Fixes 8 | - ~magithub-cache-write-to-disk~ now will write to the disk without 9 | prompting when ~require-final-newline~ is set to t. [[PR:343]] 10 | - ~magithub-github-hosts~ can now support more than one host when 11 | customized through the customization interface [[https://github.com/vermiculus/magithub/pull/357][PR:357]] 12 | - ~magithub--api-available-p~ now authenticates as =magithub= to retrieve 13 | ratelimit information. [[BUG:363]] 14 | - ~magithub-instrument~ is introduced, which see. 15 | * Enhancements 16 | - Open and close issues with =O= and =C=, respectively. [[PR:369]] 17 | - Browse files with ~magithub-browse-file~. Supports file-visiting 18 | buffers with active regions as well as dired- and magit-status-like 19 | buffers. Blame the file with ~magithub-browse-file-blame~. [[PR:377]] 20 | - Speed github datetime parsing. [[PR:393]] 21 | -------------------------------------------------------------------------------- /images/ci-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/ci-failure.png -------------------------------------------------------------------------------- /images/ci-pending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/ci-pending.png -------------------------------------------------------------------------------- /images/ci-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/ci-success.png -------------------------------------------------------------------------------- /images/create.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/create.gif -------------------------------------------------------------------------------- /images/pull-request.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/pull-request.gif -------------------------------------------------------------------------------- /images/scr1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/scr1.png -------------------------------------------------------------------------------- /images/scr2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/scr2.png -------------------------------------------------------------------------------- /images/scr3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/scr3.png -------------------------------------------------------------------------------- /images/scr4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/scr4.png -------------------------------------------------------------------------------- /images/scr5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/scr5.png -------------------------------------------------------------------------------- /images/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vermiculus/magithub/dd62c7057155c0a334e6d9087779a2923d2300b8/images/status.png -------------------------------------------------------------------------------- /magithub-ci.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-ci.el --- Show CI status as a magit-status header -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: tools 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Provide the CI status of "origin" in the Magit status buffer. 24 | 25 | ;;; Code: 26 | 27 | (require 'magit) 28 | (require 'magit-section) 29 | (require 'dash) 30 | (require 's) 31 | 32 | (require 'magithub-core) 33 | (require 'magithub-issue) 34 | 35 | ;;;###autoload 36 | (defun magithub-maybe-insert-ci-status-header () 37 | "If this is a GitHub repository, insert the CI status header." 38 | (when (and (magithub-settings-include-status-p) 39 | (magithub-usable-p) 40 | (let ((b (magit-get-current-branch))) 41 | (or (magit-get-upstream-remote b) 42 | (magit-get-push-remote b)))) 43 | (magithub-insert-ci-status-header))) 44 | 45 | (defvar magithub-ci--status-last-refreshed nil 46 | "An alist of alists: repos to refs to times. 47 | For efficiency, repos are represented only by their full names.") 48 | 49 | (defun magithub-ci--status-last-refreshed-time (repo ref) 50 | "The last time the statuses for REPO@REF were retrieved. 51 | This is a generalized variable and can be set with `setf'." 52 | (declare (gv-setter 53 | (lambda (time) 54 | `(let ((repo (magithub-repo-name ,repo))) 55 | (if-let ((statuses (assoc repo magithub-ci--status-last-refreshed))) 56 | (if-let ((status (assoc ,ref (cdr statuses)))) 57 | (setcdr status ,time) 58 | (push (cons ,ref ,time) (cdr statuses))) 59 | (push (cons repo (list (cons ,ref ,time))) 60 | magithub-ci--status-last-refreshed)))))) 61 | '(thread-last magithub-ci--status-last-refreshed 62 | (assoc (magithub-repo-name repo)) (cdr) 63 | (assoc ref) (cdr)) 64 | (cdr (assoc ref (cdr (assoc (magithub-repo-name repo) 65 | magithub-ci--status-last-refreshed))))) 66 | 67 | (defun magithub-pull-request-pr->branch (pull-request) 68 | "Does not handle cases where the local branch has been renamed." 69 | (let-alist pull-request .head.ref)) 70 | 71 | (define-error 'magithub-error-ambiguous-branch "Ambiguous Branch" 'magithub-error) 72 | (defun magithub-pull-request-branch->pr--ghub (branch) 73 | "This is a hueristic; it's not 100% accurate. 74 | It may fail if the fork has multiple branches named BRANCH." 75 | (let ((repo (magithub-repo-from-remote (magit-get-push-remote branch)))) 76 | (when (alist-get 'fork repo) 77 | (let* ((guess-head (format "%s:%s" (magit-get-push-remote branch) branch)) 78 | (prs (magithub-cache :ci-status 79 | `(magithub-request 80 | (ghubp-get-repos-owner-repo-pulls ',(magithub-repo) :head ,guess-head))))) 81 | (pcase (length prs) 82 | (0) ; this branch does not seem to correspond to any PR 83 | (1 (magit-set (number-to-string (alist-get 'number (car prs))) 84 | "branch" branch "magithub" "sourcePR") 85 | (car prs)) 86 | (_ ;; todo: currently unhandled 87 | (signal 'magithub-error-ambiguous-branch 88 | (list :prs prs 89 | :guess-head guess-head 90 | :repo-from-remote (alist-get 'full_name repo) 91 | :source-repo (alist-get 'full_name (magithub-repo)))))))))) 92 | 93 | (defun magithub-pull-request-branch->pr--gitconfig (branch) 94 | "Gets a pull request object from branch.BRANCH.magithub.sourcePR" 95 | (when-let ((source (magit-get "branch" branch "magithub" "sourcePR"))) 96 | (magithub-pull-request (magithub-repo) (string-to-number source)))) 97 | 98 | (defun magithub-ci-status--get-default-ref (&optional branch) 99 | "The ref to use for CI status based on BRANCH. 100 | 101 | Handles cases where the local branch's name is different than its 102 | remote counterpart." 103 | (setq branch (or branch (magit-get-current-branch))) 104 | (if (or (magithub-pull-request-branch->pr--gitconfig branch) 105 | (and (magithub-online-p) 106 | (with-demoted-errors "Error: %S" 107 | (magithub-pull-request-branch->pr--ghub branch)))) 108 | (magit-rev-parse branch) 109 | (when-let ((push-branch (magit-get-push-branch branch))) 110 | (when (magit-branch-p push-branch) 111 | (cdr (magit-split-branch-name push-branch)))))) 112 | 113 | (defun magithub-ci-status (ref) 114 | (when (stringp ref) 115 | (if (magit-rebase-in-progress-p) 116 | ;; avoid rate-limiting ourselves 117 | (magithub-debug-message "skipping CI status checks while in rebase") 118 | (or (magithub-cache :ci-status 119 | `(magithub-request 120 | (ghubp-get-repos-owner-repo-commits-ref-status 121 | ',(magithub-repo) ,ref)) 122 | :message 123 | (format "Getting CI status for %s..." 124 | (if (magit-branch-p ref) (format "branch `%s'" ref) 125 | (substring ref 0 6))) 126 | :after-update 127 | (lambda (status &rest _) 128 | (setf (magithub-ci--status-last-refreshed-time (magithub-repo) ref) 129 | (current-time)) 130 | (message "(magithub): new statuses retrieved -- overall: %s" 131 | (alist-get 'state status)))) 132 | '((state . "error") 133 | (total_count . 0) 134 | (magithub-message . "ref not found on remote")))))) 135 | 136 | (defvar magithub-ci-status-alist 137 | '((nil . ((display . "None") (face . magithub-ci-no-status))) 138 | ("error" . ((display . "Error") (face . magithub-ci-error))) 139 | ("failure" . ((display . "Failure") (face . magithub-ci-failure))) 140 | ("pending" . ((display . "Pending") (face . magithub-ci-pending))) 141 | ("success" . ((display . "Success") (face . magithub-ci-success))))) 142 | (defconst magithub-ci-status--unknown 143 | '((face . magithub-ci-unknown))) 144 | 145 | (defun magithub-ci-pr-status (pr) 146 | (interactive (list (thing-at-point 'github-pull-request))) 147 | (unless pr 148 | (user-error "no pr at point")) 149 | (message "state of #%d: %s" 150 | (let-alist pr .number) 151 | (let-alist (ghubp-get-repos-owner-repo-commits-ref-status 152 | (magithub-repo) 153 | (let-alist pr .head.sha)) 154 | .state))) 155 | 156 | (defun magithub-ci-visit (ref) 157 | "Jump to CI with `browse-url'." 158 | (interactive (list (magit-rev-parse (magit-commit-at-point)))) 159 | (let (done) 160 | (when (null ref) 161 | (pcase (oref (magit-current-section) value) 162 | (`(magithub-ci-url . ,url) 163 | (browse-url url) 164 | (setq done t)) 165 | (`(magithub-ci-ref . ,secref) 166 | (setq ref secref)))) 167 | (unless done 168 | (let* ((urls (alist-get 'statuses (magithub-ci-status ref))) 169 | (status 170 | (cond 171 | ((= 1 (length urls)) (car urls)) 172 | (urls (magithub--completing-read 173 | "Status service: " urls 174 | #'magithub-ci--format-status))))) 175 | (let-alist status 176 | (when (or (null .target_url) (string= "" .target_url)) 177 | (user-error "No Status URL detected")) 178 | (browse-url .target_url)))))) 179 | 180 | (defun magithub-ci--format-status (status) 181 | (let-alist status 182 | (format "(%s) %s: %s" 183 | (let ((spec (magithub-ci--status-spec .state))) 184 | (alist-get 'display spec .state)) 185 | .context 186 | .description))) 187 | 188 | (defvar magit-magithub-ci-status-section-map 189 | (let ((map (make-sparse-keymap))) 190 | (set-keymap-parent map magithub-map) 191 | (define-key map [remap magit-visit-thing] #'magithub-ci-visit) 192 | (define-key map [remap magithub-browse-thing] #'magithub-ci-visit) 193 | (define-key map [remap magit-refresh] #'magithub-ci-refresh) 194 | map) 195 | "Keymap for `magithub-ci-status' header section.") 196 | 197 | (defun magithub-ci-refresh () 198 | "Invalidate the CI cache and refresh the buffer." 199 | (interactive) 200 | (unless (magithub-online-p) 201 | (magithub-confirm 'ci-refresh-when-offline)) 202 | (magithub-cache-without-cache :ci-status 203 | (magithub-ci-status (magithub-ci-status--get-default-ref))) 204 | (magit-refresh)) 205 | 206 | (defun magithub-insert-ci-status-header () 207 | (let* ((ref (magithub-ci-status--get-default-ref)) 208 | (checks (magithub-ci-status ref)) 209 | (indent (make-string 10 ?\ ))) 210 | (when checks 211 | (magit-insert-section (magithub-ci-status 212 | `(magithub-ci-ref . ,ref) 213 | 'collapsed) 214 | (magit-insert-heading 215 | (format "%-10s%s %s %s%s" "Status:" 216 | (magithub-ci--status-header checks) 217 | (propertize "on ref" 'face 'magit-dimmed) 218 | (propertize ref 'face 'magit-refname) 219 | (propertize "..." 'face 'magit-dimmed))) 220 | (magit-insert-section-body 221 | (insert (propertize 222 | (format "%-10sas of %s\n" "" 223 | (if-let ((time (magithub-ci--status-last-refreshed-time (magithub-repo) ref))) 224 | (magithub--format-time time) 225 | "???")) 226 | 'face 'magit-dimmed)) 227 | (dolist (status (alist-get 'statuses checks)) 228 | (magit-insert-section (magithub-ci-status 229 | `(magithub-ci-url . ,(alist-get 'target_url status))) 230 | (insert indent (magithub-ci--status-propertized status "*")) 231 | (magit-insert-heading)))))))) 232 | 233 | (defun magithub-ci--status-header (checks) 234 | (pcase (alist-get 'total_count checks) 235 | (0 (format "%s %s" 236 | (magithub-ci--status-propertized checks) 237 | (or (alist-get 'magithub-message checks) 238 | (propertize "it seems checks have not yet begun" 239 | 'face 'magit-dimmed)))) 240 | (1 (magithub-ci--status-propertized checks)) 241 | (_ (let* ((overall-status (alist-get 'state checks)) 242 | (status-spec (magithub-ci--status-spec overall-status)) 243 | (display (or (alist-get 'display status-spec) overall-status)) 244 | (statuses (alist-get 'statuses checks)) 245 | (passed (-filter (lambda (s) (string= "success" (alist-get 'state s))) 246 | statuses))) 247 | (propertize (format "%s (%d/%d)" display (length passed) (length statuses)) 248 | 'face (alist-get 'face status-spec)))))) 249 | 250 | (defun magithub-ci--status-spec (status-string) 251 | (or (cdr (assoc-string status-string magithub-ci-status-alist)) 252 | magithub-ci-status--unknown)) 253 | 254 | (defun magithub-ci--status-propertized (status &optional override-status-text) 255 | (let ((status-string (alist-get 'state status)) 256 | (description (alist-get 'description status)) 257 | (context (alist-get 'context status))) 258 | (let-alist (magithub-ci--status-spec status-string) 259 | (concat (propertize (or override-status-text 260 | .display 261 | status-string) 262 | 'face .face) 263 | (when description 264 | (format " %s" description)) 265 | (when context 266 | (propertize (format " %s" context) 267 | 'face 'magit-dimmed)))))) 268 | 269 | (provide 'magithub-ci) 270 | ;;; magithub-ci.el ends here 271 | -------------------------------------------------------------------------------- /magithub-comment.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-comment.el --- tools for comments -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: lisp 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Tools for working with issue comments. 24 | 25 | ;;; Code: 26 | 27 | (require 'magit) 28 | (require 'markdown-mode) 29 | (require 'thingatpt) 30 | 31 | (require 'magithub-core) 32 | (require 'magithub-repo) 33 | (require 'magithub-issue) 34 | (require 'magithub-edit-mode) 35 | 36 | (declare-function magithub-issue-view "magithub-issue-view.el" (issue)) 37 | 38 | (defvar magit-magithub-comment-section-map 39 | (let ((m (make-sparse-keymap))) 40 | (set-keymap-parent m magithub-map) 41 | (define-key m [remap magithub-browse-thing] #'magithub-comment-browse) 42 | (define-key m [remap magit-delete-thing] #'magithub-comment-delete) 43 | (define-key m (kbd "SPC") #'magithub-comment-view) 44 | (define-key m [remap magithub-reply-thing] #'magithub-comment-reply) 45 | (define-key m [remap magithub-edit-thing] #'magithub-comment-edit) 46 | m)) 47 | 48 | (defun magithub-comment-browse (comment) 49 | (interactive (list (thing-at-point 'github-comment))) 50 | (unless comment 51 | (user-error "No comment found")) 52 | (let-alist comment 53 | (browse-url .html_url))) 54 | 55 | (declare-function face-remap-remove-relative "face-remap.el" (cookie)) 56 | (defun magithub-comment-delete (comment) 57 | (interactive (list (thing-at-point 'github-comment))) 58 | (unless comment 59 | (user-error "No comment found")) 60 | (let ((repo (magithub-comment-source-repo comment)) 61 | (author (let-alist comment .user.login)) 62 | (me (let-alist (magithub-user-me) .login))) 63 | (unless (or (string= author me) 64 | (magithub-repo-admin-p repo)) 65 | (user-error "You don't have permission to delete this comment")) 66 | (let ((cookie (face-remap-add-relative 'magit-section-highlight 67 | ;;'magit-diff-removed-highlight 68 | ;;:strike-through t 69 | ;;:background "red4" 70 | ;; 71 | 'magithub-deleted-thing 72 | ))) 73 | (unwind-protect (magithub-confirm 'comment-delete) 74 | (face-remap-remove-relative cookie))) 75 | (magithub-request 76 | (ghubp-delete-repos-owner-repo-issues-comments-id repo comment)) 77 | (magithub-cache-without-cache :issues 78 | (magit-refresh-buffer)) 79 | (message "Comment deleted"))) 80 | 81 | (defun magithub-comment-source-issue (comment) 82 | (magithub-cache :comment 83 | `(magithub-request 84 | (ghubp-follow-get ,(alist-get 'issue_url comment))))) 85 | 86 | (defun magithub-comment-source-repo (comment) 87 | (magithub-issue-repo (magithub-comment-source-issue comment))) 88 | 89 | (defun magithub-comment-insert (comment) 90 | "Insert a single issue COMMENT." 91 | (let-alist comment 92 | (magit-insert-section (magithub-comment comment) 93 | (magit-insert-heading (propertize .user.login 'face 'magithub-user)) 94 | (save-excursion 95 | (let ((created-at (magithub--format-time .created_at))) 96 | (backward-char 1) 97 | (insert (make-string (- (current-fill-column) 98 | (current-column) 99 | (length created-at)) 100 | ? )) 101 | (insert (propertize created-at 'face 'magit-dimmed)))) 102 | (insert (magithub-fill-gfm (magithub-wash-gfm (s-trim .body))) 103 | "\n\n")))) 104 | 105 | (defvar magithub-gfm-view-mode-map 106 | (let ((m (make-sparse-keymap))) 107 | (define-key m [remap kill-this-buffer] #'magithub-comment-view-close) 108 | m) 109 | "Keymap for `magithub-gfm-view-mode'.") 110 | 111 | (declare-function gfm-view-mode "ext:markdown-mode.el") 112 | (define-derived-mode magithub-gfm-view-mode gfm-view-mode "M:GFM-View" 113 | "Major mode for viewing GitHub markdown content.") 114 | 115 | (defvar-local magithub-comment-view--parent-buffer nil 116 | "The 'parent' buffer of the current comment-view. 117 | This variable is used to jump back to the issue that contained 118 | the comment; see `magithub-comment-view' and 119 | `magithub-comment-view-close'.") 120 | 121 | (defun magithub-comment-view (comment) 122 | "View COMMENT in a new buffer." 123 | (interactive (list (thing-at-point 'github-comment))) 124 | (let ((prev (current-buffer))) 125 | (with-current-buffer (get-buffer-create "*comment*") 126 | (magithub-gfm-view-mode) 127 | (setq-local magithub-comment-view--parent-buffer prev) 128 | (let ((inhibit-read-only t)) 129 | (erase-buffer) 130 | (insert (magithub-wash-gfm (alist-get 'body comment)))) 131 | (goto-char 0) 132 | (magit-display-buffer (current-buffer))))) 133 | 134 | (defun magithub-comment-view-close () 135 | "Close the current buffer." 136 | (interactive) 137 | (let ((oldbuf magithub-comment-view--parent-buffer)) 138 | (kill-this-buffer) 139 | (magit-display-buffer oldbuf))) 140 | 141 | ;;;###autoload 142 | (defun magithub-comment-new (issue &optional discard-draft initial-content) 143 | "Comment on ISSUE in a new buffer. 144 | If prefix argument DISCARD-DRAFT is specified, the draft will not 145 | be considered. 146 | 147 | If INITIAL-CONTENT is specified, it will be inserted as the 148 | initial contents of the reply if there is no draft." 149 | (interactive (let ((issue (magithub-interactive-issue))) 150 | (prog1 (list issue current-prefix-arg) 151 | (unless (derived-mode-p 'magithub-issue-view-mode) 152 | (magithub-issue-view issue))))) 153 | (let* ((issueref (magithub-issue-reference issue)) 154 | (repo (magithub-issue-repo issue))) 155 | (with-current-buffer 156 | (magithub-edit-new (concat "reply to " issueref) 157 | :header (concat "replying to " issueref) 158 | :submit #'magithub-issue-comment-submit 159 | :content initial-content 160 | :prompt-discard-draft discard-draft 161 | :file (magithub-comment--draft-file issue repo)) 162 | (setq-local magithub-issue issue) 163 | (setq-local magithub-repo repo) 164 | (magit-display-buffer (current-buffer))))) 165 | 166 | (defun magithub-comment--draft-file (issue repo) 167 | "Get an appropriate draft file for ISSUE in REPO." 168 | (let-alist issue 169 | (expand-file-name (format "%s-comment" .number) 170 | (magithub-repo-data-dir repo)))) 171 | 172 | (defun magithub-comment--submit-edit (comment repo new-body) 173 | (interactive (list (thing-at-point 'github-comment) 174 | (thing-at-point 'github-repository) 175 | (buffer-string))) 176 | (when (string= new-body "") 177 | (user-error "Can't post an empty comment; try deleting it instead")) 178 | (magithub-confirm 'comment-edit) 179 | (magithub-request 180 | (ghubp-patch-repos-owner-repo-issues-comments-id 181 | repo comment 182 | `((body . ,new-body))))) 183 | 184 | (defun magithub-comment-edit (comment issue repo) 185 | "Edit COMMENT." 186 | (interactive (list (thing-at-point 'github-comment) 187 | (or (thing-at-point 'github-issue) 188 | (thing-at-point 'github-pull-request)) 189 | (thing-at-point 'github-repository))) 190 | (let ((updated (magithub-request (ghubp-follow-get (alist-get 'url comment))))) 191 | (with-current-buffer 192 | (magithub-edit-new (format "*%s: editing comment by %s (%s)*" 193 | (magithub-issue-reference issue) 194 | (let-alist comment .user.login) 195 | (alist-get 'id comment)) 196 | :submit #'magithub-comment--submit-edit 197 | :content (alist-get 'body updated) 198 | :file (magithub-comment--draft-file issue repo)) 199 | (setq-local magithub-issue issue) 200 | (setq-local magithub-repo repo) 201 | (setq-local magithub-comment updated) 202 | (magit-display-buffer (current-buffer))) 203 | 204 | (unless (string= (alist-get 'body comment) 205 | (alist-get 'body updated)) 206 | (message "Comment has changed since information was cached; \ 207 | updated content pulled in for edit")))) 208 | 209 | (defun magithub-comment-reply (comment &optional discard-draft issue) 210 | "Reply to COMMENT on ISSUE. 211 | If prefix argument DISCARD-DRAFT is provided, the current draft 212 | will deleted. 213 | 214 | If ISSUE is not provided, it will be determined from context or 215 | from COMMENT." 216 | (interactive (list (thing-at-point 'github-comment) 217 | current-prefix-arg 218 | (thing-at-point 'github-issue))) 219 | (let-alist comment 220 | (magithub-comment-new 221 | (or issue (magithub-request (ghubp-follow-get .issue_url))) 222 | discard-draft 223 | (let ((reply-body (if (use-region-p) 224 | (buffer-substring (region-beginning) (region-end)) 225 | .body))) 226 | (with-temp-buffer 227 | (insert (string-trim (magithub-wash-gfm reply-body))) 228 | (markdown-blockquote-region (point-min) (point-max)) 229 | (goto-char (point-max)) 230 | (insert "\n\n") 231 | (buffer-string)))))) 232 | 233 | (defun magithub-issue-comment-submit (issue comment &optional repo) 234 | "On ISSUE, submit a new COMMENT. 235 | 236 | COMMENT is the text of the new comment. 237 | 238 | REPO is an optional repo object; it will be deduced from ISSUE if 239 | not provided." 240 | (interactive (list (thing-at-point 'github-issue) 241 | (save-restriction 242 | (widen) 243 | (buffer-substring-no-properties (point-min) (point-max))) 244 | (thing-at-point 'github-repository))) 245 | (unless issue 246 | (user-error "No issue provided")) 247 | (setq repo (or repo 248 | (magithub-issue-repo issue) 249 | (thing-at-point 'github-repository))) 250 | (unless repo 251 | (user-error "No repo detected")) 252 | ;; all required args provided 253 | (magithub-confirm 'comment (magithub-issue-reference issue)) 254 | (magithub-request 255 | (ghubp-post-repos-owner-repo-issues-number-comments 256 | repo issue `((body . ,comment)))) 257 | (magithub-edit-delete-draft) 258 | (message "Success")) 259 | 260 | (provide 'magithub-comment) 261 | ;;; magithub-comment.el ends here 262 | -------------------------------------------------------------------------------- /magithub-completion.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-completion.el --- Completion using info provided by magithub -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2018 Steve Purcell 4 | 5 | ;; Author: Steve Purcell 6 | ;; Keywords: convenience 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Provides `completion-at-point' functions which complete issue 24 | ;; numbers etc when they are entered in commit messages. 25 | 26 | ;; Extended information is attached to completions so that `company' 27 | ;; can access it via the standard `company-capf' backend. 28 | 29 | ;;; Code: 30 | 31 | 32 | (require 'magithub-settings) 33 | (require 'magithub-issue) 34 | 35 | 36 | ;;;###autoload 37 | (defun magithub-completion-complete-issues () 38 | "A `completion-at-point' function which completes \"#123\" issue references. 39 | Add this to `completion-at-point-functions' in buffers where you 40 | want this to be available." 41 | (when (magithub-enabled-p) 42 | (when (looking-back "#\\([0-9]*\\)" (- (point) 10)) 43 | (let ((start (match-beginning 1)) 44 | (end (match-end 0)) 45 | (prefix (match-string 1)) 46 | completions) 47 | (dolist (i (magithub--issue-list)) 48 | (let-alist i 49 | (let ((n (number-to-string .number))) 50 | (when (string-prefix-p prefix n) 51 | (push (propertize n :issue i) completions))))) 52 | (list start end (sort completions #'string<) 53 | :exclusive 'no 54 | :company-docsig (lambda (c) 55 | (let-alist (get-text-property 0 :issue c) 56 | .title)) 57 | :annotation-function (lambda (c) 58 | (let-alist (get-text-property 0 :issue c) 59 | .title)) 60 | :company-doc-buffer (lambda (c) 61 | (save-window-excursion 62 | (magithub-issue-visit 63 | (get-text-property 0 :issue c))))))))) 64 | 65 | ;;;###autoload 66 | (defun magithub-completion-complete-users () 67 | "A `completion-at-point' function which completes \"@user\" user references. 68 | Add this to `completion-at-point-functions' in buffers where you 69 | want this to be available. The user list is currently simply the 70 | list of all users who created issues or pull requests." 71 | (when (magithub-enabled-p) 72 | (when (looking-back "@\\([_-A-Za-z0-9]*\\)" (- (point) 30)) 73 | (let ((start (match-beginning 1)) 74 | (end (match-end 0)) 75 | (prefix (match-string 1)) 76 | completions) 77 | (dolist (i (magithub--issue-list)) 78 | (let-alist i 79 | (when (string-prefix-p prefix .user.login) 80 | (let ((candidate (copy-sequence .user.login)) 81 | (association (and .author_association 82 | (not (string= "NONE" .author_association)) 83 | .author_association))) 84 | (push (propertize candidate :user .user :association association) 85 | completions))))) 86 | (list start end (sort (cl-remove-duplicates completions :test #'string=) 87 | #'string<) 88 | :exclusive 'no 89 | :company-docsig (lambda (c) (get-text-property 0 :association c)) 90 | :annotation-function (lambda (c) (get-text-property 0 :association c))))))) 91 | 92 | ;;;###autoload 93 | (defun magithub-completion-enable () 94 | "Enable completion of info from magithub in the current buffer." 95 | (make-local-variable 'completion-at-point-functions) 96 | (dolist (f '(magithub-completion-complete-issues 97 | magithub-completion-complete-users)) 98 | (add-to-list 'completion-at-point-functions f))) 99 | 100 | 101 | (provide 'magithub-completion) 102 | ;;; magithub-completion.el ends here 103 | -------------------------------------------------------------------------------- /magithub-dash.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-dash.el --- magithub dashboard -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: hypermedia 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Magithub-Dash is a dashboard for your GitHub activity. 24 | 25 | ;;; Code: 26 | 27 | (require 'magit) 28 | (require 'magithub-core) 29 | (require 'magithub-notification) 30 | (require 'magithub-issue) 31 | 32 | (declare-function magithub-dispatch-popup "magithub.el") 33 | 34 | (defcustom magithub-dashboard-show-read-notifications t 35 | "Show read notifications in the dashboard." 36 | :type 'boolean 37 | :group 'magithub) 38 | 39 | (transient-define-prefix magithub-dashboard-popup () 40 | "Popup console for the dashboard." 41 | ["Actions" 42 | ("r" "Toggle showing read notifications" magithub-dashboard-show-read-notifications-toggle)]) 43 | 44 | (defun magithub-dashboard-show-read-notifications-toggle () 45 | (interactive) 46 | (setq magithub-dashboard-show-read-notifications 47 | (not magithub-dashboard-show-read-notifications)) 48 | (magit-refresh-buffer)) 49 | 50 | ;;;###autoload 51 | (defun magithub-dashboard () 52 | "View your GitHub dashboard." 53 | (interactive) 54 | (let ((magit-generate-buffer-name-function 55 | (lambda (&rest _) "*magithub-dash*"))) 56 | (magit-mode-setup #'magithub-dash-mode))) 57 | 58 | (defvaralias 'magithub-dash-map 'magithub-dash-mode-map 59 | "Old name of `magithub-dash-mode-map'. 60 | This will be removed in a future version.") 61 | (defvar magithub-dash-mode-map 62 | (let ((m (make-sparse-keymap))) 63 | (set-keymap-parent m magit-mode-map) 64 | (define-key m (kbd "5") #'magit-section-show-level-5) 65 | (define-key m (kbd "M-5") #'magit-section-show-level-5-all) 66 | (define-key m (kbd ";") #'magithub-dashboard-popup) 67 | (define-key m (kbd "H") #'magithub-dispatch-popup) 68 | m) 69 | "Keymap for `magithub-dash-mode'.") 70 | ;; todo: remove on version bump 71 | 72 | (define-derived-mode magithub-dash-mode 73 | magit-mode "Magithub-Dash" 74 | "Major mode for your GitHub dashboard.") 75 | 76 | (defun magithub-dash-refresh-buffer (&rest _args) 77 | "Refresh the dashboard. 78 | Runs `magithub-dash-sections-hook'." 79 | (interactive) 80 | (magit-insert-section (magithub-dash-buf) 81 | (run-hooks 'magithub-dash-sections-hook)) 82 | (let ((inhibit-read-only t)) 83 | (save-excursion 84 | (goto-char (point-max)) 85 | (delete-blank-lines)))) 86 | 87 | (defvar magithub-dash-sections-hook 88 | '(magithub-dash-insert-headers 89 | magithub-dash-insert-notifications 90 | magithub-dash-insert-issues) 91 | "Sections inserted by `magithub-dashboard'.") 92 | 93 | (defvar magithub-dash-headers-hook 94 | '(magithub-dash-insert-user-name-header 95 | magithub-dash-insert-ratelimit-header 96 | magithub-maybe-report-offline-mode) 97 | "Headers inserted by `magithub-dash-insert-headers'.") 98 | 99 | (defun magithub-dash-insert-headers () 100 | "Insert dashboard headers. 101 | See also `magithub-dash-headers-hook'." 102 | (magit-insert-headers 'magithub-dash-headers-hook)) 103 | 104 | (defun magithub-dash-insert-user-name-header (&optional user) 105 | "Inserts a header for USER's name and login." 106 | (setq user (or user (magithub-user-me))) 107 | (let-alist user 108 | (when .login 109 | (let ((login (propertize .login 'face 'magithub-user))) 110 | (magit-insert-section (magithub-user user) 111 | (insert (format "%-10s" "User:") 112 | (if .name 113 | (format "%s (%s)" .name login) 114 | login) 115 | "\n")))))) 116 | 117 | (defun magithub-dash-insert-ratelimit-header () 118 | "If API requests are being rate-limited, insert relevant information." 119 | (magithub-request 120 | (when-let ((ratelimit (ghubp-ratelimit))) 121 | (when (time-less-p (alist-get 'reset ratelimit) (current-time)) 122 | (ghubp-ratelimit 'no-headers))) 123 | (let-alist (ghubp-ratelimit) 124 | (when .limit 125 | (magit-insert-section (magithub-ratelimit) 126 | (let* ((seconds-until-reset (time-to-seconds 127 | (time-subtract .reset 128 | (current-time)))) 129 | (ratio (/ (float .remaining) .limit))) 130 | (insert 131 | (format "%-10s%s - %d/%d requests; %s until reset\n" "Requests:" 132 | (cond 133 | ((< 0.50 ratio) (propertize "OK" 'face 'success)) 134 | ((< 0.25 ratio) (propertize "Running low..." 'face 'warning)) 135 | (t (propertize "Danger!" 'face 'error))) 136 | .remaining 137 | .limit 138 | (magithub-cache--time-out seconds-until-reset))))))))) 139 | 140 | (defun magithub-dash-insert-notifications (&optional notifications) 141 | "Insert NOTIFICATIONS into the buffer bucketed by repository." 142 | (setq notifications (or notifications 143 | (magithub-notifications 144 | magithub-dashboard-show-read-notifications))) 145 | (if notifications 146 | (let* ((bucketed (magithub-core-bucket 147 | notifications 148 | (apply-partially #'alist-get 'repository))) 149 | (unread (if magithub-dashboard-show-read-notifications 150 | (-filter #'magithub-notification-unread-p notifications) 151 | notifications)) 152 | (hide (not unread)) 153 | (heading (if magithub-dashboard-show-read-notifications 154 | (format "%s (%d unread of %d)" 155 | (propertize "Notifications" 156 | 'face 'magit-section-heading) 157 | (length unread) 158 | (length notifications)) 159 | (format "%s (%d)" 160 | (propertize "Notifications" 161 | 'face 'magit-section-heading) 162 | (length notifications))))) 163 | (magit-insert-section (magithub-notifications notifications hide) 164 | (magit-insert-heading heading) 165 | (magithub-for-each-bucket bucketed repo repo-notifications 166 | (setq hide (null (-filter #'magithub-notification-unread-p 167 | repo-notifications))) 168 | (magit-insert-section (magithub-repo repo hide) 169 | (magit-insert-heading 170 | (concat (propertize (magithub-repo-name repo) 'face 'magithub-repo) 171 | (propertize "..." 'face 'magit-dimmed))) 172 | (mapc #'magithub-notification-insert-section repo-notifications) 173 | (insert "\n"))) 174 | (insert "\n"))) 175 | (magit-insert-section (magithub-notifications) 176 | (magit-insert-heading "Notifications") 177 | (insert (propertize (if magithub-dashboard-show-read-notifications 178 | "No notifications" 179 | "No unread notifications") 180 | 'face 'magit-dimmed) 181 | "\n\n")))) 182 | 183 | (defun magithub-dash-insert-issues (&optional issues title) 184 | "Insert ISSUES bucketed by their source repository. 185 | 186 | If ISSUES is not defined, all issues assigned to the current user 187 | will be used." 188 | (magithub-request 189 | (setq issues (or issues (magithub-cache :issues `(magithub-request 190 | (ghubp-get-issues)))) 191 | title (or title "Issues Assigned to Me")) 192 | (when-let ((user-repo-issue-buckets 193 | ;; bucket by user then by repo 194 | (magithub-core-bucket-multi issues 195 | #'magithub-issue-repo 196 | (lambda (repo) (alist-get 'owner repo))))) 197 | (magit-insert-section (magithub-users-repo-issue-buckets) 198 | (magit-insert-heading 199 | (format "%s (%d)" 200 | (propertize title 'face 'magit-section-heading) 201 | (length issues))) 202 | (magithub-for-each-bucket user-repo-issue-buckets user repo-issue-buckets 203 | (magit-insert-section (magithub-user-repo-issues) 204 | (magit-insert-heading 205 | (propertize (alist-get 'login user) 'face 'magithub-user) 206 | (propertize "/..." 'face 'magit-dimmed)) 207 | (magithub-for-each-bucket repo-issue-buckets repo repo-issues 208 | (magit-insert-section (magithub-repo-issues repo) 209 | (magit-insert-heading 210 | (format "%s:" (propertize (alist-get 'name repo) 211 | 'face 'magithub-repo))) 212 | (magithub-issue-insert-sections repo-issues) 213 | (insert "\n"))))) 214 | (insert "\n"))))) 215 | 216 | (provide 'magithub-dash) 217 | ;;; magithub-dash.el ends here 218 | -------------------------------------------------------------------------------- /magithub-edit-mode.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-edit-mode.el --- message-editing mode -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: multimedia 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Edit generic GitHub (markdown) content. To be used for comments, 24 | ;; issues, pull requests, etc. 25 | 26 | ;;; Code: 27 | 28 | (require 'markdown-mode) 29 | (require 'git-commit) 30 | 31 | (defvar magithub-edit-mode-map 32 | (let ((m (make-sparse-keymap))) 33 | (define-key m (kbd "C-c C-c") #'magithub-edit-submit) 34 | (define-key m (kbd "C-c C-k") #'magithub-edit-cancel) 35 | m) 36 | "Keymap for `magithub-edit-mode'.") 37 | 38 | ;;;###autoload 39 | (define-derived-mode magithub-edit-mode gfm-mode "Magithub-Edit" 40 | "Major mode for editing GitHub issues and pull requests.") 41 | 42 | (defvar-local magithub-edit-submit-function nil 43 | "Populated by SUBMIT in `magithub-edit-new'.") 44 | (defvar-local magithub-edit-cancel-function nil 45 | "Populated by CANCEL in `magithub-edit-new'.") 46 | (defvar-local magithub-edit-previous-buffer nil 47 | "The buffer we were in when the edit was initiated.") 48 | 49 | (defface magithub-edit-title 50 | '((t :inherit markdown-header-face-1)) 51 | "Face used for the title in issues and pull requests." 52 | :group 'magithub-faces) 53 | 54 | (defun magithub-edit-submit () 55 | "Submit this post. 56 | Uses `magithub-edit-submit-function' to do so." 57 | (interactive) 58 | (unless (commandp magithub-edit-submit-function t) 59 | (error "No submit function defined")) 60 | (magithub-edit--done magithub-edit-submit-function) 61 | (magithub-cache-without-cache t 62 | (magit-refresh-buffer))) 63 | 64 | (defun magithub-edit-cancel () 65 | "Cancel this post. 66 | Offer to save a draft if the buffer is considered modified, then 67 | call `magithub-edit-cancel-function'." 68 | (interactive) 69 | ;; Offer to save the draft 70 | (if (and (buffer-modified-p) 71 | ;; don't necessarily want to use `magithub-confirm', here 72 | ;; this is potentially a very dangerous action 73 | (y-or-n-p "Save draft? ")) 74 | (save-buffer) 75 | (set-buffer-modified-p nil)) 76 | 77 | ;; If the saved draft is empty, might as well delete it 78 | (when (and (stringp buffer-file-name) 79 | (file-readable-p buffer-file-name) 80 | (string= "" (let ((f buffer-file-name)) 81 | (with-temp-buffer 82 | (insert-file-contents f) 83 | (buffer-string))))) 84 | (magithub-edit-delete-draft)) 85 | 86 | (magithub-edit--done magithub-edit-cancel-function)) 87 | 88 | (defun magithub-edit--done (callback) 89 | "Cleanup this buffer. 90 | If CALLBACK is a command, call it interactively. (This will 91 | usually be the SUBMIT or CANCEL commands from 92 | `magithub-edit-new'.) If that function returns a buffer, switch 93 | to that buffer." 94 | (let ((nextbuf magithub-edit-previous-buffer)) 95 | (when (commandp callback t) 96 | (let ((newbuf (save-excursion 97 | (call-interactively callback)))) 98 | (when (bufferp newbuf) 99 | (setq nextbuf newbuf)))) 100 | (set-buffer-modified-p nil) 101 | (kill-buffer) 102 | (when nextbuf 103 | (let ((switch-to-buffer-preserve-window-point t)) 104 | (switch-to-buffer nextbuf))))) 105 | 106 | (defun magithub-edit-delete-draft () 107 | "Delete the draft for the current edit buffer." 108 | (when (and (stringp buffer-file-name) 109 | (file-exists-p buffer-file-name) 110 | (file-writable-p buffer-file-name)) 111 | (delete-file buffer-file-name magit-delete-by-moving-to-trash) 112 | (message "Deleted %s" buffer-file-name)) 113 | (set-visited-file-name nil)) 114 | 115 | (cl-defun magithub-edit-new (buffer-name &key cancel content file header prompt-discard-draft submit template) 116 | "Generate a new edit buffer called BUFFER-NAME and return it. 117 | 'Edit' buffers provide a common interface and handling for 118 | submitting, cancelling, and saving drafts of posts. 119 | 120 | CANCEL is a function to use upon \\[magithub-edit-cancel]. 121 | 122 | CONTENT is initial content for the buffer. It is considered 123 | novel and the buffer will not be closed without prompting to save 124 | a draft. 125 | 126 | FILE is the file to use for drafts of this post. 127 | 128 | HEADER is a title to use in the header line of the new buffer. 129 | 130 | If PROMPT-DISCARD-DRAFT is non-nil, this function will display 131 | the draft before offering to delete it. This option is 132 | recommended when using \\[universal-argument] with the command 133 | that calls this function. 134 | 135 | SUBMIT is a function to use upon \\[magithub-edit-submit]. 136 | 137 | TEMPLATE is like CONTENT, but is not considered novel. We won't 138 | ask to save a draft here if post is cancelled." 139 | (declare (indent 1)) 140 | (let ((prevbuf (current-buffer)) 141 | (file (and (stringp file) 142 | (file-writable-p file) 143 | file)) 144 | draft) 145 | 146 | ;; Load the draft 147 | (setq draft (and (stringp file) 148 | (file-readable-p file) 149 | (with-temp-buffer 150 | (insert-file-contents file) 151 | (buffer-string)))) 152 | (when (string= draft "") 153 | (setq draft nil)) 154 | 155 | ;; Discard the draft if desired 156 | (when (and draft prompt-discard-draft) 157 | (with-current-buffer (get-buffer-create " *draft*") 158 | (erase-buffer) 159 | (insert draft) 160 | (view-buffer-other-window (current-buffer)) 161 | ;; don't necessarily want to use `magithub-confirm', here 162 | ;; this is potentially a very dangerous action 163 | (when (yes-or-no-p "Discard this draft? ") 164 | (setq draft nil) 165 | (when (file-writable-p file) 166 | (delete-file file magit-delete-by-moving-to-trash))) 167 | (kill-buffer (current-buffer)))) 168 | 169 | (with-current-buffer (get-buffer-create buffer-name) 170 | (when file 171 | (let ((orig-name (buffer-name)) 172 | (dir default-directory)) 173 | (set-visited-file-name file) 174 | (rename-buffer orig-name) 175 | (cd dir))) 176 | (magithub-edit-mode) 177 | 178 | (setq magithub-edit-previous-buffer prevbuf 179 | magithub-edit-submit-function submit 180 | magithub-edit-cancel-function cancel) 181 | (magit-set-header-line-format 182 | (substitute-command-keys 183 | (let ((line "submit: \\[magithub-edit-submit] | cancel: \\[magithub-edit-cancel]")) 184 | (when header 185 | (setq line (concat line " | " header))) 186 | line))) 187 | 188 | (cond 189 | (draft 190 | (insert draft) 191 | (set-buffer-modified-p nil) 192 | (goto-char (point-max)) 193 | (message "Loaded existing draft from %s" file)) 194 | (content 195 | (insert content) 196 | (goto-char (point-max)) 197 | (message "Loaded initial content")) 198 | (template 199 | (insert template) 200 | (set-buffer-modified-p nil) 201 | (goto-char (point-min)) 202 | (message "Loaded template"))) 203 | 204 | (current-buffer)))) 205 | 206 | (provide 'magithub-edit-mode) 207 | ;;; magithub-edit-mode.el ends here 208 | -------------------------------------------------------------------------------- /magithub-faces.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-faces.el --- faces of Magithub -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: faces 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Holds all faces for Magithub. 24 | 25 | ;;; Code: 26 | 27 | (require 'faces) 28 | (require 'magit) 29 | (require 'git-commit) 30 | 31 | (defface magithub-repo 32 | '((t :inherit magit-branch-remote)) 33 | "Face used for repository names." 34 | :group 'magithub-faces) 35 | 36 | (defface magithub-issue-title 37 | '((t)) 38 | "Face used for issue titles." 39 | :group 'magithub-faces) 40 | 41 | (defface magithub-issue-number 42 | '((t :inherit magit-dimmed)) 43 | "Face used for issue numbers." 44 | :group 'magithub-faces) 45 | 46 | (defface magithub-issue-title-edit 47 | '((t :inherit magithub-issue-title :inherit (git-commit-summary))) 48 | "Face used for post titles during editing." 49 | :group 'magithub-faces) 50 | 51 | (defface magithub-issue-title-with-note 52 | '((t :inherit magithub-issue-title :inherit (git-commit-summary))) 53 | "Face used for issue titles when the issue has an attached note. 54 | See also `magithub-issue-personal-note'." 55 | :group 'magithub-faces) 56 | 57 | (defface magithub-user 58 | '((t :inherit magit-log-author)) 59 | "Face used for usernames." 60 | :group 'magithub-faces) 61 | 62 | (defface magithub-ci-no-status 63 | '((t :inherit magit-dimmed)) 64 | "Face used when CI status is `no-status'." 65 | :group 'magithub-faces) 66 | 67 | (defface magithub-ci-error 68 | '((t :inherit magit-signature-untrusted)) 69 | "Face used when CI status is `error'." 70 | :group 'magithub-faces) 71 | 72 | (defface magithub-ci-pending 73 | '((t :inherit magit-signature-untrusted)) 74 | "Face used when CI status is `pending'." 75 | :group 'magithub-faces) 76 | 77 | (defface magithub-ci-success 78 | '((t :inherit success)) 79 | "Face used when CI status is `success'." 80 | :group 'magithub-faces) 81 | 82 | (defface magithub-ci-failure 83 | '((t :inherit error)) 84 | "Face used when CI status is `failure'" 85 | :group 'magithub-faces) 86 | 87 | (defface magithub-ci-unknown 88 | '((t :inherit magit-signature-untrusted)) 89 | "Face used when CI status is `unknown'." 90 | :group 'magithub-faces) 91 | 92 | (defface magithub-issue-open 93 | '((t :inherit success)) 94 | "Face used to indicate an issue is open." 95 | :group 'magithub-faces) 96 | 97 | (defface magithub-issue-closed 98 | '((t :inherit error)) 99 | "Face used to indicate an issue is closed." 100 | :group 'magithub-faces) 101 | 102 | (defface magithub-label '((t :box t)) 103 | "The inherited face used for labels. 104 | Feel free to customize any part of this face, but be aware that 105 | `:foreground' will be overridden by `magithub-label-propertize'." 106 | :group 'magithub) 107 | 108 | (defface magithub-notification-reason 109 | '((t :inherit magit-dimmed)) 110 | "Face used for notification reasons." 111 | :group 'magithub-faces) 112 | 113 | (defface magithub-deleted-thing 114 | '((t :background "red4" :inherit magit-section-highlight)) 115 | "Face used for things about to be deleted." 116 | :group 'magithub-faces) 117 | 118 | (provide 'magithub-faces) 119 | ;;; magithub-faces.el ends here 120 | -------------------------------------------------------------------------------- /magithub-issue-post.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-issue-post.el --- -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | 7 | ;; This program is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation, either version 3 of the License, or 10 | ;; (at your option) any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; You should have received a copy of the GNU General Public License 18 | ;; along with this program. If not, see . 19 | 20 | ;;; Commentary: 21 | 22 | ;;; Code: 23 | 24 | (require 'magithub-core) 25 | (require 'magithub-repo) 26 | (require 'magithub-issue) 27 | (require 'magithub-label) 28 | (require 'magithub-edit-mode) 29 | 30 | (declare-function magithub-issue-view "magithub-issue-view.el" (issue)) 31 | 32 | (defvar-local magithub-issue--extra-data nil) 33 | 34 | (defun magithub-issue-post-submit () 35 | (interactive) 36 | (let ((issue (magithub-issue-post--parse-buffer)) 37 | (repo (magithub-repo))) 38 | (when (s-blank-p (alist-get 'title issue)) 39 | (user-error "Title is required")) 40 | (when (magithub-repo-push-p repo) 41 | (when-let ((issue-labels (magithub-label-read-labels "Labels: "))) 42 | (push (cons 'labels issue-labels) issue))) 43 | (magithub-confirm 'submit-issue) 44 | (let ((issue (magithub-request 45 | (ghubp-post-repos-owner-repo-issues repo issue)))) 46 | (magithub-edit-delete-draft) 47 | (magithub-issue-view issue)))) 48 | 49 | (defun magithub-issue-post--parse-buffer () 50 | (let ((lines (split-string (buffer-string) "\n"))) 51 | `((title . ,(s-trim (car lines))) 52 | (body . ,(s-trim (mapconcat #'identity (cdr lines) "\n")))))) 53 | 54 | (defun magithub-issue-new (repo) 55 | (interactive (list (magithub-repo))) 56 | (let* ((repo (magithub-repo repo)) 57 | (name (magithub-repo-name repo))) 58 | (with-current-buffer 59 | (magithub-edit-new (format "*magithub-issue: %s*" name) 60 | :header (format "Creating an issue for %s" name) 61 | :submit #'magithub-issue-post-submit 62 | :file (expand-file-name "new-issue-draft" 63 | (magithub-repo-data-dir repo)) 64 | :template (magithub-issue--template-text "ISSUE_TEMPLATE")) 65 | (font-lock-add-keywords nil `((,(rx bos (group (*? any)) eol) 1 66 | 'magithub-issue-title-edit t))) 67 | (magithub-bug-reference-mode-on) 68 | (magit-display-buffer (current-buffer))))) 69 | 70 | (defun magithub-pull-request-new-from-issue 71 | (repo issue base head &optional maintainer-can-modify) 72 | "Create a pull request from an existing issue. 73 | REPO is the parent repository of ISSUE. BASE and HEAD are as 74 | they are in `magithub-pull-request-new'." 75 | (interactive (if-let ((issue-at-point (thing-at-point 'github-issue))) 76 | (let-alist (magithub-pull-request-new-arguments) 77 | (let ((allow-maint-mod (magithub-confirm-no-error 78 | 'pr-allow-maintainers-to-submit))) 79 | (magithub-confirm 'submit-pr-from-issue 80 | (magithub-issue-reference issue-at-point) 81 | .user+head .base) 82 | (list .repo issue-at-point .base .head allow-maint-mod))) 83 | (user-error "No issue detected at point"))) 84 | (let ((pull-request `((head . ,head) 85 | (base . ,base) 86 | (issue . ,(alist-get 'number issue))))) 87 | (when maintainer-can-modify 88 | (push (cons 'maintainer_can_modify t) pull-request)) 89 | (magithub-request 90 | (ghubp-post-repos-owner-repo-pulls repo pull-request)))) 91 | 92 | (defun magithub-issue--template-text (template) 93 | (with-temp-buffer 94 | (when-let ((template (magithub-issue--template-find template))) 95 | (insert-file-contents template) 96 | (buffer-string)))) 97 | 98 | (defun magithub-issue--template-find (filename) 99 | "Find an appropriate template called FILENAME and returns its absolute path. 100 | 101 | See also URL 102 | `https://github.com/blog/2111-issue-and-pull-request-templates'" 103 | (let ((default-directory (magit-toplevel)) 104 | combinations) 105 | (dolist (tryname (list filename (concat filename ".md"))) 106 | (dolist (trydir (list default-directory (expand-file-name ".github/"))) 107 | (push (expand-file-name tryname trydir) combinations))) 108 | (-find #'file-readable-p combinations))) 109 | 110 | (defun magithub-remote-branches (remote) 111 | "Return a list of branches on REMOTE." 112 | (let ((regexp (concat (regexp-quote remote) (rx "/" (group (* any)))))) 113 | (--map (and (string-match regexp it) 114 | (match-string 1 it)) 115 | (magit-list-remote-branch-names remote)))) 116 | 117 | (defun magithub-remote-branches-choose (prompt remote &optional default) 118 | "Using PROMPT, choose a branch on REMOTE." 119 | (let ((branches (magithub-remote-branches remote))) 120 | (magit-completing-read 121 | (format "[%s] %s" 122 | (magithub-repo-name (magithub-repo-from-remote remote)) 123 | prompt) 124 | branches 125 | nil t nil nil (and (member default branches) default)))) 126 | 127 | (defun magithub-pull-request-new-arguments () 128 | (unless (magit-get-push-remote) 129 | (user-error "Nothing on remote yet; have you pushed your branch? Aborting")) 130 | (let* ((this-repo (magithub-read-repo "Fork's remote (this is you!) " (ghubp-username) t)) 131 | (this-repo-owner (let-alist this-repo .owner.login)) 132 | (parent-repo (or (alist-get 'parent this-repo) this-repo)) 133 | (this-remote (car (magithub-repo-remotes-for-repo this-repo))) 134 | (on-this-remote (string= (magit-get-push-remote) this-remote)) 135 | (base-remote (car (magithub-repo-remotes-for-repo parent-repo))) 136 | (head-branch (let ((branch (magithub-remote-branches-choose 137 | "Head branch" this-remote 138 | (when on-this-remote 139 | (magit-get-current-branch))))) 140 | (unless (magit-rev-verify (magit-get-push-branch branch)) 141 | (user-error "`%s' has not yet been pushed to your fork (%s)" 142 | branch (magithub-repo-name this-repo))) 143 | branch)) 144 | (base (magithub-remote-branches-choose 145 | "Base branch" base-remote 146 | (or (and on-this-remote 147 | (magit-get-upstream-branch head-branch)) 148 | (let-alist parent-repo .default_branch)))) 149 | (user+head (format "%s:%s" this-repo-owner head-branch))) 150 | (when (magithub-request (ghubp-get-repos-owner-repo-pulls parent-repo nil 151 | :head user+head)) 152 | (user-error "A pull request on %s already exists for head \"%s\"" 153 | (magithub-repo-name parent-repo) 154 | user+head)) 155 | `((repo . ,parent-repo) 156 | (base . ,base) 157 | (head . ,(if (string= this-remote base-remote) 158 | head-branch 159 | user+head)) 160 | (head-no-user . ,head-branch) 161 | (fork . ,this-repo) 162 | (user+head . ,user+head)))) 163 | 164 | (defun magithub-pull-request-new (repo base head head-no-user) 165 | "Create a new pull request." 166 | (interactive (let-alist (magithub-pull-request-new-arguments) 167 | (magithub-confirm 'pre-submit-pr .user+head 168 | (magithub-repo-name .repo) .base) 169 | (list .repo .base .head .head-no-user))) 170 | (let ((is-single-commit 171 | (string= "1" (magit-git-string "rev-list" "--count" (format "%s.." base))))) 172 | (unless is-single-commit 173 | (apply #'magit-log-other (list (format "%s..%s" base head)) (magit-log-arguments))) 174 | (with-current-buffer 175 | (let ((template (magithub-issue--template-text "PULL_REQUEST_TEMPLATE"))) 176 | (magithub-edit-new (format "*magithub-pull-request: %s into %s:%s*" 177 | head 178 | (magithub-repo-name repo) 179 | base) 180 | :header (let-alist repo (format "PR %s/%s (%s->%s)" 181 | .owner.login .name head base)) 182 | :submit #'magithub-pull-request-submit 183 | :file (expand-file-name "new-pull-request-draft" 184 | (magithub-repo-data-dir repo)) 185 | :template template 186 | :content (when is-single-commit 187 | ;; when we only want to merge one commit 188 | ;; insert that commit message as the initial content 189 | (concat 190 | (with-temp-buffer 191 | (magit-git-insert "show" "-q" head-no-user "--format=%B") 192 | (let ((fill-column (point-max))) 193 | (fill-region (point-min) (point-max)) 194 | (buffer-string))) 195 | template)))) 196 | (font-lock-add-keywords nil `((,(rx bos (group (*? any)) eol) 1 197 | 'magithub-issue-title-edit t))) 198 | (magithub-bug-reference-mode-on) 199 | (setq magithub-issue--extra-data 200 | `((base . ,base) (head . ,head) (repo . ,repo))) 201 | (magit-display-buffer (current-buffer))))) 202 | 203 | (defun magithub-pull-request-submit () 204 | (interactive) 205 | (let ((pull-request `(,@(magithub-issue-post--parse-buffer) 206 | (base . ,(alist-get 'base magithub-issue--extra-data)) 207 | (head . ,(alist-get 'head magithub-issue--extra-data))))) 208 | (when (s-blank-p (alist-get 'title pull-request)) 209 | (user-error "Title is required")) 210 | (magithub-confirm 'submit-pr) 211 | (when (magithub-confirm-no-error 'pr-allow-maintainers-to-submit) 212 | (push (cons 'maintainer_can_modify t) pull-request)) 213 | (let ((pr (condition-case _ 214 | (magithub-request 215 | (ghubp-post-repos-owner-repo-pulls 216 | (alist-get 'repo magithub-issue--extra-data) 217 | pull-request)) 218 | (ghub-422 219 | (user-error "This pull request already exists!"))))) 220 | (magithub-edit-delete-draft) 221 | (magithub-issue-view pr)))) 222 | 223 | (provide 'magithub-issue-post) 224 | ;;; magithub-issue-post.el ends here 225 | -------------------------------------------------------------------------------- /magithub-issue-tricks.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-issue-tricks.el --- -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | 7 | ;; This program is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation, either version 3 of the License, or 10 | ;; (at your option) any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; You should have received a copy of the GNU General Public License 18 | ;; along with this program. If not, see . 19 | 20 | ;;; Commentary: 21 | 22 | ;;; Code: 23 | 24 | (require 'magit) 25 | (require 'magithub-issue) 26 | 27 | (defcustom magithub-hub-executable "hub" 28 | "The hub executable used by Magithub." 29 | :group 'magithub 30 | :package-version '(magithub . "0.1") 31 | :type 'string) 32 | 33 | (defmacro magithub-with-hub (&rest body) 34 | `(let ((magit-git-executable magithub-hub-executable) 35 | (magit-pre-call-git-hook nil) 36 | (magit-git-global-arguments nil)) 37 | ,@body)) 38 | 39 | ;;;###autoload 40 | (defun magithub-pull-request-merge (pull-request &optional args) 41 | "Merge PULL-REQUEST with ARGS. 42 | See `magithub-pull-request--completing-read'. If point is on a 43 | pull-request object, that object is selected by default." 44 | (interactive (list (magithub-issue-completing-read-pull-requests) 45 | (magit-am-arguments))) 46 | (unless (executable-find magithub-hub-executable) 47 | (user-error "This hasn't been supported in elisp yet; please install/configure `hub'")) 48 | (unless (member pull-request (magithub-pull-requests)) 49 | (user-error "Unknown pull request %S" pull-request)) 50 | (let-alist pull-request 51 | (magithub-with-hub 52 | (magit-run-git-sequencer "am" args .html_url)) 53 | (message "#%d has been applied" .number))) 54 | 55 | (provide 'magithub-issue-tricks) 56 | ;;; magithub-issue-tricks.el ends here 57 | -------------------------------------------------------------------------------- /magithub-issue-view.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-issue-view.el --- view issues -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: lisp 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; View issues in magit-like buffers. 24 | 25 | ;;; Code: 26 | 27 | (require 'magit-mode) 28 | 29 | (require 'magithub-core) 30 | (require 'magithub-issue) 31 | (require 'magithub-comment) 32 | 33 | (defvar magithub-issue-view-mode-map 34 | (let ((m (make-composed-keymap (list magithub-map) magit-mode-map))) 35 | (define-key m [remap magithub-reply-thing] #'magithub-comment-new) 36 | (define-key m [remap magithub-browse-thing] #'magithub-issue-browse) 37 | (define-key m [remap magit-refresh] #'magithub-issue-view-refresh) 38 | m)) 39 | 40 | (define-derived-mode magithub-issue-view-mode magit-mode 41 | "Issue View" "View GitHub issues with Magithub.") 42 | 43 | (defvar magithub-issue-view-headers-hook 44 | '(magithub-issue-view-insert-title 45 | magithub-issue-view-insert-author 46 | magithub-issue-view-insert-state 47 | magithub-issue-view-insert-asked 48 | magithub-issue-view-insert-labels) 49 | "Header information for each issue.") 50 | 51 | (defvar magithub-issue-view-sections-hook 52 | '(magithub-issue-view-insert-headers 53 | magithub-issue-view-insert-body 54 | magithub-issue-view-insert-comments) 55 | "Sections to be inserted for each issue.") 56 | 57 | (defun magithub-issue-view-refresh () 58 | "Refresh the current issue." 59 | (interactive) 60 | (if (derived-mode-p 'magithub-issue-view-mode) 61 | (progn 62 | ;; todo: find a better means to separate the keymaps of issues 63 | ;; in the status buffer vs issues in their own buffer 64 | (when magithub-issue 65 | (magithub-cache-without-cache :issues 66 | (setq-local magithub-issue 67 | (magithub-issue magithub-repo magithub-issue)) 68 | (magithub-issue-comments magithub-issue))) 69 | (let ((magit-refresh-args (list magithub-issue))) 70 | (magit-refresh)) 71 | (message "refreshed")) 72 | (call-interactively #'magit-refresh))) 73 | 74 | (defun magithub-issue-view-refresh-buffer (issue &rest _) 75 | (setq-local magithub-issue issue) 76 | (setq-local magithub-repo (magithub-issue-repo issue)) 77 | (magit-insert-section (magithub-issue issue) 78 | (run-hooks 'magithub-issue-view-sections-hook))) 79 | 80 | (defun magithub-issue-view-insert-headers () 81 | "Run `magithub-issue-view-headers-hook'." 82 | (magit-insert-headers 'magithub-issue-view-headers-hook)) 83 | 84 | (defun magithub-issue-view--lock-value (args) 85 | "Provide an identifying value for ISSUE. 86 | See also `magit-buffer-lock-functions'." 87 | (let ((issue (car args))) 88 | (let-alist `((repo . ,(magithub-issue-repo issue)) 89 | (issue . ,issue)) 90 | (list .repo.owner.login .repo.name .issue.number)))) 91 | (push (cons 'magithub-issue-view-mode #'magithub-issue-view--lock-value) 92 | magit-buffer-lock-functions) 93 | 94 | (defun magithub-issue-view--buffer-name (_mode issue-lock-value) 95 | "Generate a buffer name for ISSUE-LOCK-VALUE. 96 | See also `magithub-issue-view--lock-value'." 97 | (let ((owner (nth 0 issue-lock-value)) 98 | (repo (nth 1 issue-lock-value)) 99 | (number (nth 2 issue-lock-value))) 100 | (format "*magithub: %s/%s#%d: %s*" 101 | owner 102 | repo 103 | number 104 | (alist-get 'title (magithub-issue `((owner (login . ,owner)) 105 | (name . ,repo)) 106 | number))))) 107 | 108 | ;;;###autoload 109 | (defun magithub-issue-view (issue) 110 | "View ISSUE in a new buffer. 111 | Return the new buffer." 112 | (interactive (list (magithub-interactive-issue))) 113 | (let ((magit-generate-buffer-name-function #'magithub-issue-view--buffer-name)) 114 | (magit-mode-setup-internal #'magithub-issue-view-mode (list issue) t) 115 | (current-buffer))) 116 | 117 | (cl-defun magithub-issue-view-insert--generic (title text &optional type section-value &key face) 118 | "Insert a generic header line with TITLE: VALUE" 119 | (declare (indent 2)) 120 | (setq type (or type 'magithub)) 121 | (magit-insert-section ((eval type) section-value) 122 | (insert (format "%-10s" title) 123 | (or (and face (propertize text 'face face)) 124 | text) 125 | ?\n) 126 | (magit-insert-heading))) 127 | 128 | (defun magithub-issue-view-insert-title () 129 | "Insert issue title." 130 | (let-alist magithub-issue 131 | (magithub-issue-view-insert--generic "Title:" .title))) 132 | 133 | (defun magithub-issue-view-insert-author () 134 | "Insert issue author." 135 | (insert (format "%-10s" "Author:")) 136 | (let-alist magithub-issue 137 | (magit-insert-section (magithub-user .user) 138 | (insert (propertize .user.login 'face 'magithub-user) ?\n) 139 | (magit-insert-heading)))) 140 | 141 | (defun magithub-issue-view-insert-state () 142 | "Insert issue state." 143 | (magithub-issue-view-insert--generic "State:" 144 | (if (magithub-issue-open-p magithub-issue) 145 | (propertize "Open" 'face 'magithub-issue-open) 146 | (propertize "Closed" 'face 'magithub-issue-closed)) 147 | :face 'magit-dimmed)) 148 | 149 | (defun magithub-issue-view-insert-asked () 150 | "Insert posted time." 151 | (let-alist magithub-issue 152 | (magithub-issue-view-insert--generic "Posted:" (magithub--format-time .created_at) 153 | :face 'magit-dimmed))) 154 | 155 | (defun magithub-issue-view-insert-labels () 156 | "Insert labels." 157 | (insert (format "%-10s" "Labels:")) 158 | (magithub-label-insert-list (alist-get 'labels magithub-issue)) 159 | (insert ?\n)) 160 | 161 | (defun magithub-issue-view-insert-body () 162 | "Insert issue body." 163 | (let-alist magithub-issue 164 | (magit-insert-section (magithub-issue-body magithub-issue) 165 | (magit-insert-heading "Body") 166 | (if (or (null .body) (string= .body "")) 167 | (insert (propertize "There's nothing here!\n\n" 'face 'magit-dimmed)) 168 | (insert (magithub-fill-gfm (magithub-wash-gfm (s-trim .body))) "\n\n"))))) 169 | 170 | (defun magithub-issue-view-insert-comments () 171 | "Insert issue comments." 172 | (let ((comments (magithub-issue-comments magithub-issue))) 173 | (magit-insert-section (magithub-issue-comments comments) 174 | (magit-insert-heading "Comments:") 175 | (if (null comments) 176 | (insert (propertize "There's nothing here!\n\n" 'face 'magit-dimmed)) 177 | (mapc #'magithub-comment-insert comments))))) 178 | 179 | (provide 'magithub-issue-view) 180 | ;;; magithub-issue-view.el ends here 181 | -------------------------------------------------------------------------------- /magithub-issue.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-issue.el --- Browse issues with Magithub -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: tools 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Jump to issues from `magit-status'! 24 | 25 | ;;; Code: 26 | 27 | (require 's) 28 | (require 'dash) 29 | (require 'ghub+) 30 | (require 'cl-lib) 31 | (require 'magit) 32 | (require 'thingatpt) 33 | 34 | (require 'magithub-core) 35 | (require 'magithub-user) 36 | (require 'magithub-label) 37 | 38 | (declare-function magithub-issue-view "magithub-issue-view.el" (issue)) 39 | 40 | (defvar magit-magithub-repo-issues-section-map 41 | (let ((m (make-sparse-keymap))) 42 | (define-key m [remap magit-visit-thing] #'magithub-repo-visit-issues) 43 | m)) 44 | 45 | (defvar magit-magithub-note-section-map 46 | (let ((m (make-sparse-keymap))) 47 | (set-keymap-parent m magithub-map) 48 | (define-key m [remap magit-visit-thing] #'magithub-issue-personal-note) 49 | m)) 50 | 51 | ;;; Core 52 | 53 | (defun magithub-issue--admin-p (issue) 54 | "Returns non-nil if ISSUE can be managed by the current user." 55 | (magithub-request 56 | (or (string= (let-alist issue .user.login) 57 | (let-alist (magithub-user-me) .login)) 58 | (magithub-repo-admin-p)))) 59 | 60 | (defun magithub-issue--ensure-admin (issue user-error-message) 61 | "Ensure the user can administrate ISSUE. 62 | If not, error out with USER-ERROR-MESSAGE." 63 | (declare (indent 1)) 64 | (unless (magithub-issue--admin-p issue) 65 | (user-error "%s: not the issue owner or an administrator of this repo" user-error-message))) 66 | 67 | (defun magithub-issue-open-p (issue) 68 | "Returns non-nil if ISSUE is open." 69 | (string= (let-alist issue .state) "open")) 70 | 71 | (declare-function magithub-issue-view-refresh "magithub-issue-view") 72 | (defun magithub-issue--open-close (issue do-close) 73 | "Open or close ISSUE. If DO-CLOSE is non-nil, ISSUE will be closed." 74 | (magithub-issue--ensure-admin issue 75 | (if do-close 76 | "Cannot close this issue" 77 | "Cannot reopen this issue")) 78 | ;; valid states: issue is open and we want to close 79 | ;; issue is closed and we want to open 80 | ;; (not ...) is to coerce to t or nil 81 | (unless (eq (not (magithub-issue-open-p issue)) (not do-close)) 82 | (user-error (if do-close 83 | "Issue already closed" 84 | "Issue already open"))) 85 | (magithub-confirm (if do-close 'issue-close 'issue-reopen) 86 | (magithub-issue-reference issue)) 87 | (prog1 88 | (magithub-request 89 | (ghubp-patch-repos-owner-repo-issues-number 90 | (magithub-repo) issue 91 | `((state . ,(if do-close "closed" "open"))))) 92 | (when (derived-mode-p 'magithub-issue-view-mode) 93 | (require 'magithub-issue-view) 94 | (magithub-issue-view-refresh)) 95 | (magithub-message "%s %s" 96 | (if do-close "closed" "reopened") 97 | (magithub-issue-reference issue)))) 98 | 99 | (defun magithub-issue-close (issue) 100 | "Close ISSUE." 101 | (interactive (list (magithub-interactive-issue))) 102 | (magithub-issue--open-close issue t)) 103 | 104 | (defun magithub-issue-open (issue) 105 | "Open ISSUE." 106 | (interactive (list (magithub-interactive-issue))) 107 | (magithub-issue--open-close issue nil)) 108 | 109 | (defmacro magithub-interactive-issue-or-pr (sym args doc &rest body) 110 | "Declare an interactive form that works on both issues and PRs. 111 | SYM is a postfix for the function symbol. An appropriate prefix 112 | will be added for both the issue-version and PR-version. 113 | 114 | ARGS should be a list of one element, the symbol ISSUE-OR-PR. 115 | 116 | DOC is a doc-string. 117 | 118 | BODY is the function implementation." 119 | (declare (indent defun) 120 | (doc-string 3)) 121 | (unless (eq (car args) 'issue-or-pr) 122 | (error "For clarity, the first argument must be ISSUE-OR-PR")) 123 | (let* ((snam (symbol-name sym)) 124 | (isym (intern (concat "magithub-issue-" snam))) 125 | (psym (intern (concat "magithub-pull-request-" snam)))) 126 | `(list 127 | (defun ,isym ,(cons 'issue (cdr args)) 128 | ,(format (concat doc "\n\nSee also `%S'.") "ISSUE" psym) 129 | (interactive (list (magithub-interactive-issue))) 130 | (let ((issue-or-pr issue)) 131 | ,@body)) 132 | (defun ,psym ,(cons 'pull-request (cdr args)) 133 | ,(format (concat doc "\n\nSee also `%S'.") "PULL-REQUEST" isym) 134 | (interactive (list (magithub-interactive-pull-request))) 135 | (let ((issue-or-pr pull-request)) 136 | ,@body))))) 137 | 138 | (defun magithub--issue-list (&rest params) 139 | "Return a list of issues for the current repository. 140 | The response is unpaginated, so avoid doing this with PARAMS that 141 | will return a ton of issues. 142 | 143 | See also `ghubp-get-repos-owner-repo-issues'." 144 | (cl-assert (cl-evenp (length params))) 145 | (magithub-cache :issues 146 | `(magithub-request 147 | (ghubp-unpaginate 148 | (ghubp-get-repos-owner-repo-issues 149 | ',(magithub-repo) 150 | ,@params))) 151 | :message 152 | "Retrieving issue list...")) 153 | 154 | (defun magithub-issue--issue-is-pull-p (issue) 155 | (not (null (alist-get 'pull_request issue)))) 156 | 157 | (defun magithub-issue--issue-is-issue-p (issue) 158 | (and (alist-get 'number issue) 159 | (not (magithub-issue--issue-is-pull-p issue)))) 160 | 161 | (defun magithub-issue-comments (issue) 162 | "Get comments on ISSUE." 163 | (let ((repo (magithub-issue-repo issue))) 164 | (magithub-cache :issues 165 | `(magithub-request 166 | (ghubp-unpaginate 167 | (ghubp-get-repos-owner-repo-issues-number-comments ',repo ',issue)))))) 168 | 169 | ;;; Finding issues and pull requests 170 | 171 | (defun magithub-issues () 172 | "Return a list of issue objects that are actually issues." 173 | (-filter #'magithub-issue--issue-is-issue-p 174 | (magithub--issue-list))) 175 | 176 | (defun magithub-pull-requests () 177 | "Return a list of issue objects that are actually pull requests." 178 | (-filter #'magithub-issue--issue-is-pull-p 179 | (magithub--issue-list))) 180 | 181 | ;;; Sorting 182 | 183 | (defcustom magithub-issue-sort-function 184 | #'magithub-issue-sort-ascending 185 | "Function used for sorting issues and pull requests in the 186 | status buffer. Should take two issue-objects as arguments." 187 | :type 'function 188 | :group 'magithub) 189 | 190 | (magithub-defsort magithub-issue-sort-ascending #'< 191 | "Lower issue numbers come first." 192 | (apply-partially #'alist-get :number)) 193 | 194 | (magithub-defsort magithub-issue-sort-descending #'> 195 | "Higher issue numbers come first." 196 | (apply-partially #'alist-get :number)) 197 | 198 | (defun magithub-issue--sort (issues) 199 | "Sort ISSUES by `magithub-issue-sort-function'." 200 | (sort issues magithub-issue-sort-function)) 201 | 202 | ;;; Getting issues from the user 203 | 204 | (defun magithub-issue--format-for-read (issue) 205 | "Format ISSUE as a string suitable for completion." 206 | (let-alist issue (format "%3d %s" .number .title))) 207 | 208 | (defun magithub-issue--completing-read (prompt default preds) 209 | "Complete over all open pull requests returning its issue object. 210 | If point is on a pull-request object, that object is selected by 211 | default." 212 | (magithub--completing-read prompt (magithub--issue-list) 213 | #'magithub-issue--format-for-read 214 | (apply-partially #'magithub--satisfies-p preds) 215 | t default)) 216 | (defun magithub-issue-completing-read-issues (&optional default) 217 | "Read an issue in the minibuffer with completion." 218 | (interactive (list (thing-at-point 'github-issue))) 219 | (magithub-issue--completing-read 220 | "Issue: " default (list #'magithub-issue--issue-is-issue-p))) 221 | (defun magithub-issue-completing-read-pull-requests (&optional default) 222 | "Read a pull request in the minibuffer with completion." 223 | (interactive (list (thing-at-point 'github-pull-request))) 224 | (magithub-issue--completing-read 225 | "Pull Request: " default (list #'magithub-issue--issue-is-pull-p))) 226 | (defun magithub-interactive-issue () 227 | (or (thing-at-point 'github-issue) 228 | (magithub-issue-completing-read-issues))) 229 | (defun magithub-interactive-pull-request () 230 | (or (thing-at-point 'github-pull-request) 231 | (magithub-issue-completing-read-pull-requests))) 232 | 233 | (defun magithub-issue-find (number) 234 | "Return the issue or pull request with the given NUMBER." 235 | (-find (lambda (i) (= (alist-get 'number i) number)) 236 | (magithub--issue-list :filter "all" :state "all"))) 237 | 238 | (defun magithub-issue (repo number-or-issue) 239 | "Retrieve in REPO issue NUMBER-OR-ISSUE. 240 | NUMBER-OR-ISSUE is either a number or an issue object. If it's a 241 | number, the issue by that number is retrieved. If it's an issue 242 | object, the same issue is retrieved." 243 | (let ((num (or (and (numberp number-or-issue) 244 | number-or-issue) 245 | (alist-get 'number number-or-issue)))) 246 | (magithub-cache :issues 247 | `(magithub-request 248 | (ghubp-get-repos-owner-repo-issues-number 249 | ',repo '((number . ,num)))) 250 | :message 251 | (format "Getting issue %s#%d..." (magithub-repo-name repo) num)))) 252 | 253 | (defun magithub-issue-personal-note-file (issue-or-pr) 254 | "Return an absolute filename appropriate for ISSUE-OR-PR." 255 | (let-alist `((repo . ,(magithub-repo (magithub-issue-repo issue-or-pr))) 256 | (issue . ,issue-or-pr)) 257 | (expand-file-name 258 | (format "%s/%s/notes/%d.org" .repo.owner.login .repo.name .issue.number) 259 | magithub-dir))) 260 | 261 | (magithub-interactive-issue-or-pr personal-note (issue-or-pr) 262 | "Write a personal note about %s. 263 | This is stored in `magit-git-dir' and is unrelated to 264 | `git-notes'." 265 | (if (null issue-or-pr) 266 | (error "No issue or pull request here") 267 | (let-alist issue-or-pr 268 | (let ((note-file (magithub-issue-personal-note-file issue-or-pr))) 269 | (make-directory (file-name-directory note-file) t) 270 | (with-current-buffer (find-file-other-window note-file) 271 | (rename-buffer (format "*magithub note for #%d*" .number))))))) 272 | 273 | (defun magithub-issue-has-personal-note-p (issue-or-pr) 274 | "Non-nil if a personal note exists for ISSUE-OR-PR." 275 | (let ((filename (magithub-issue-personal-note-file issue-or-pr))) 276 | (and (file-exists-p filename) 277 | (not (string-equal 278 | "" 279 | (string-trim 280 | (with-temp-buffer 281 | (insert-file-contents-literally filename) 282 | (buffer-string)))))))) 283 | 284 | (defun magithub-issue-repo (issue) 285 | "Get a repository object from ISSUE." 286 | (let-alist issue 287 | (or .repository 288 | .base.repo 289 | (save-match-data 290 | (when (string-match (concat (rx bos) 291 | "https://" 292 | (regexp-quote (ghubp-host)) 293 | (rx "/repos/" 294 | (group (+ (not (any "/")))) "/" 295 | (group (+ (not (any "/")))) "/issues/") 296 | (regexp-quote (number-to-string .number)) 297 | (rx eos)) 298 | .url) 299 | (magithub-repo 300 | `((owner (login . ,(match-string 1 .url))) 301 | (name . ,(match-string 2 .url))))))))) 302 | 303 | (defun magithub-issue-reference (issue) 304 | "Return a string like \"owner/repo#number\" for ISSUE." 305 | (let-alist `((repo . ,(magithub-issue-repo issue)) 306 | (issue . ,issue)) 307 | (format "%s/%s#%d" .repo.owner.login .repo.name .issue.number))) 308 | 309 | (defun magithub-issue-from-reference (string) 310 | "Parse an issue from an \"owner/repo#number\" STRING." 311 | (when (string-match (rx bos (group (+ any)) 312 | "/" (group (+ any)) 313 | "#" (group (+ digit)) 314 | eos) 315 | string) 316 | (magithub-issue `((owner (login . ,(match-string 1 string))) 317 | (name . ,(match-string 2 string))) 318 | (string-to-number (match-string 3 string))))) 319 | 320 | (defun magithub-issue-insert-sections (issues) 321 | "Insert ISSUES into the buffer with alignment. 322 | See also `magithub-issue-insert-section'." 323 | (let ((max-num-len (thread-last issues 324 | (ghubp-get-in-all '(number)) 325 | (apply #'max) 326 | (number-to-string) 327 | (length)))) 328 | (--map (magithub-issue-insert-section it max-num-len) 329 | issues))) 330 | 331 | (defun magithub-issue-insert-section (issue &optional pad-num-to-len) 332 | "Insert ISSUE into the buffer. 333 | If PAD-NUM-TO-LEN is non-nil, it is an integer width. For 334 | example, if this section's issue number is \"3\" and the next 335 | section's number is \"401\", pass a padding of 3 to both to align 336 | them. 337 | 338 | See also `magithub-issue-insert-sections'." 339 | (when issue 340 | (setq pad-num-to-len (or pad-num-to-len 0)) 341 | (magit-insert-section (magithub-issue issue t) 342 | (let-alist issue 343 | (magit-insert-heading 344 | (format (format "%%%ds %%s" (1+ pad-num-to-len)) ;1+ accounts for # 345 | (propertize (format "#%d" .number) 346 | 'face 'magithub-issue-number) 347 | (propertize .title 348 | 'face (if (magithub-issue-has-personal-note-p issue) 349 | 'magithub-issue-title-with-note 350 | 'magithub-issue-title)))) 351 | (run-hook-with-args 'magithub-issue-details-hook issue 352 | (format " %s %%-12s" 353 | (make-string pad-num-to-len ?\ ))))))) 354 | 355 | (defvar magithub-issue-details-hook 356 | '(magithub-issue-detail-insert-personal-notes 357 | magithub-issue-detail-insert-created 358 | magithub-issue-detail-insert-updated 359 | magithub-issue-detail-insert-author 360 | magithub-issue-detail-insert-assignees 361 | magithub-issue-detail-insert-labels 362 | magithub-issue-detail-insert-body-preview) 363 | "Detail functions for issue-type sections. 364 | These details appear under issues as expandable content. 365 | 366 | Each function takes two arguments: 367 | 368 | 1. an issue object 369 | 2. a format string for a string label (for alignment)") 370 | 371 | (defun magithub-issue-detail-insert-author (issue fmt) 372 | "Insert the author of ISSUE using FMT." 373 | (let-alist issue 374 | (insert (format fmt "Author:")) 375 | (magit-insert-section (magithub-user (magithub-user .user)) 376 | (insert 377 | (propertize .user.login 'face 'magithub-user))) 378 | (insert "\n"))) 379 | 380 | (defun magithub-issue-detail-insert-created (issue fmt) 381 | "Insert when ISSUE was created using FMT." 382 | (let-alist issue 383 | (insert (format fmt "Created:") 384 | (propertize (magithub--format-time .created_at) 385 | 'face 'magit-dimmed) 386 | "\n"))) 387 | 388 | (defun magithub-issue-detail-insert-updated (issue fmt) 389 | "Insert when ISSUE was created using FMT." 390 | (let-alist issue 391 | (insert (format fmt "Updated:") 392 | (propertize (magithub--format-time .updated_at) 393 | 'face 'magit-dimmed) 394 | "\n"))) 395 | 396 | (defun magithub-issue-detail-insert-assignees (issue fmt) 397 | "Insert the assignees of ISSUE using FMT." 398 | (let-alist issue 399 | (insert (format fmt "Assignees:")) 400 | (if .assignees 401 | (let ((assignees .assignees) assignee) 402 | (while (setq assignee (pop assignees)) 403 | (magit-insert-section (magithub-assignee (magithub-user assignee)) 404 | (insert (propertize (alist-get 'login assignee) 405 | 'face 'magithub-user))) 406 | (when assignees 407 | (insert " ")))) 408 | (magit-insert-section (magithub-assignee) 409 | (insert (propertize "none" 'face 'magit-dimmed)))) 410 | (insert "\n"))) 411 | 412 | (defun magithub-issue-detail-insert-personal-notes (issue fmt) 413 | "Insert a link to ISSUE's notes." 414 | (insert (format fmt "My notes:")) 415 | (magit-insert-section (magithub-note) 416 | (insert (if (magithub-issue-has-personal-note-p issue) 417 | (propertize "visit your note" 'face 'link) 418 | (propertize "create a new note" 'face 'magit-dimmed)))) 419 | (insert "\n")) 420 | 421 | (defun magithub-issue-detail-insert-body-preview (issue fmt) 422 | "Insert a preview of ISSUE's body using FMT." 423 | (let-alist issue 424 | (let (label-string label-len width did-cut maxchar text) 425 | (setq label-string (format fmt "Preview:")) 426 | (insert label-string) 427 | 428 | (if (or (null .body) (string= .body "")) 429 | (insert (concat (propertize "none" 'face 'magit-dimmed) 430 | "\n")) 431 | 432 | (setq label-len (length label-string)) 433 | (setq width (- fill-column label-len)) 434 | (setq maxchar (* 3 width)) 435 | (setq did-cut (< maxchar (length .body))) 436 | (setq maxchar (if did-cut (- maxchar 3) maxchar)) 437 | (setq text (if did-cut 438 | (substring .body 0 (min (length .body) (* 4 width))) 439 | .body)) 440 | (setq text (replace-regexp-in-string " " "" text)) 441 | (setq text (let ((fill-column width)) 442 | (thread-last text 443 | (magithub-fill-gfm) 444 | (magithub-indent-text label-len) 445 | (s-trim)))) 446 | (insert text) 447 | (when did-cut 448 | (insert (propertize "..." 'face 'magit-dimmed))) 449 | (insert "\n"))))) 450 | 451 | (defun magithub-issue-detail-insert-labels (issue fmt) 452 | "Insert ISSUE's labels using FMT." 453 | (let-alist issue 454 | (insert (format fmt "Labels:")) 455 | (magithub-label-insert-list .labels) 456 | (insert "\n"))) 457 | 458 | ;;; Magithub-Status stuff 459 | 460 | (defun magithub-issue-refresh () 461 | "Refresh issues for this repository." 462 | (interactive) 463 | (magithub-cache-without-cache :issues 464 | (magithub--issue-list)) 465 | (when (derived-mode-p 'magit-status-mode) 466 | (magit-refresh))) 467 | 468 | (declare-function magithub-comment-new "magithub-comment") 469 | (defvar magit-magithub-issue-section-map 470 | (let ((map (make-sparse-keymap))) 471 | (set-keymap-parent map magithub-map) 472 | (define-key map [remap magit-visit-thing] #'magithub-issue-visit) 473 | (define-key map [remap magithub-browse-thing] #'magithub-issue-browse) 474 | (define-key map [remap magithub-reply-thing] #'magithub-comment-new) 475 | (define-key map "L" #'magithub-issue-add-labels) 476 | (define-key map "N" #'magithub-issue-personal-note) 477 | (define-key map "C" #'magithub-issue-close) 478 | (define-key map "O" #'magithub-issue-open) 479 | map) 480 | "Keymap for `magithub-issue' sections.") 481 | 482 | (defvar magit-magithub-issues-list-section-map 483 | (let ((map (make-sparse-keymap))) 484 | (set-keymap-parent map magithub-map) 485 | (define-key map [remap magit-visit-thing] #'magithub-issue-visit) 486 | (define-key map [remap magithub-browse-thing] #'magithub-issue-browse) 487 | (define-key map [remap magit-refresh] #'magithub-issue-refresh) 488 | map) 489 | "Keymap for `magithub-issues-list' sections.") 490 | 491 | (defvar magit-magithub-pull-request-section-map 492 | (let ((map (make-sparse-keymap))) 493 | (set-keymap-parent map magit-magithub-issues-list-section-map) 494 | (define-key map [remap magithub-issue-visit] #'magithub-pull-visit) 495 | (define-key map [remap magithub-issue-browse] #'magithub-pull-browse) 496 | map) 497 | "Keymap for `magithub-pull-request' sections.") 498 | 499 | (defvar magit-magithub-pull-requests-list-section-map 500 | (let ((map (make-sparse-keymap))) 501 | (set-keymap-parent map magithub-map) 502 | (define-key map [remap magit-visit-thing] #'magithub-pull-visit) 503 | (define-key map [remap magithub-browse-thing] #'magithub-pull-browse) 504 | (define-key map [remap magit-refresh] #'magithub-issue-refresh) 505 | map) 506 | "Keymap for `magithub-pull-request-list' sections.") 507 | 508 | ;; By maintaining these as lists of functions, we're setting 509 | ;; ourselves up to be able to dynamically apply new filters from the 510 | ;; status buffer (e.g., 'bugs' or 'questions' assigned to me) 511 | (defcustom magithub-issue-issue-filter-functions nil 512 | "List of functions that filter issues. 513 | Each function will be supplied a single issue object. If any 514 | function returns nil, the issue will not be listed in the status 515 | buffer." 516 | :type '(repeat function) 517 | :group 'magithub) 518 | 519 | (defcustom magithub-issue-pull-request-filter-functions nil 520 | "List of functions that filter pull-requests. 521 | Each function will be supplied a single issue object. If any 522 | function returns nil, the issue will not be listed in the status 523 | buffer." 524 | :type '(repeat function) 525 | :group 'magithub) 526 | 527 | (defun magithub-issue-add-labels (issue labels) 528 | "Update ISSUE's labels to LABELS." 529 | (interactive 530 | (when (magithub-verify-manage-labels t) 531 | (let* ((fmt (lambda (l) (alist-get 'name l))) 532 | (issue (or (thing-at-point 'github-issue) 533 | (thing-at-point 'github-pull-request))) 534 | (current-labels (alist-get 'labels issue)) 535 | (to-remove (magithub--completing-read-multiple 536 | "Remove labels: " current-labels fmt))) 537 | (setq current-labels (cl-set-difference current-labels to-remove)) 538 | (list issue (magithub--completing-read-multiple 539 | "Add labels: " (magithub-label-list) fmt 540 | nil nil current-labels))))) 541 | (when (magithub-request 542 | (ghubp-patch-repos-owner-repo-issues-number 543 | (magithub-repo) issue `((labels . ,labels)))) 544 | (setcdr (assq 'labels issue) labels)) 545 | (when (derived-mode-p 'magit-status-mode) 546 | (magit-refresh))) 547 | 548 | ;;;###autoload 549 | (defun magithub-issue--insert-issue-section () 550 | "Insert GitHub issues if appropriate." 551 | (when (and (magithub-settings-include-issues-p) 552 | (magithub-usable-p) 553 | (alist-get 'has_issues (magithub-repo))) 554 | (magithub-issue--insert-generic-section 555 | (magithub-issues-list) 556 | "Issues" 557 | (magithub-issues) 558 | magithub-issue-issue-filter-functions))) 559 | 560 | ;;;###autoload 561 | (defun magithub-issue--insert-pr-section () 562 | "Insert GitHub pull requests if appropriate." 563 | (when (and (magithub-settings-include-pull-requests-p) 564 | (magithub-usable-p)) 565 | (magithub-feature-maybe-idle-notify 566 | 'pull-request-merge) 567 | (magithub-issue--insert-generic-section 568 | (magithub-pull-requests-list) 569 | "Pull Requests" 570 | (magithub-pull-requests) 571 | magithub-issue-pull-request-filter-functions))) 572 | 573 | (defmacro magithub-issue--insert-generic-section 574 | (spec title list filters) 575 | (let ((sym-filtered (cl-gensym))) 576 | `(when-let ((,sym-filtered (magithub-filter-all ,filters ,list))) 577 | (magit-insert-section ,spec 578 | (insert (format "%s%s:" 579 | (propertize ,title 'face 'magit-section-heading) 580 | (if ,filters 581 | (propertize " (filtered)" 'face 'magit-dimmed) 582 | ""))) 583 | (magit-insert-heading) 584 | (magithub-issue-insert-sections ,sym-filtered) 585 | (insert ?\n))))) 586 | 587 | (defun magithub-issue-browse (issue) 588 | "Visits ISSUE in the browser. 589 | Interactively, this finds the issue at point." 590 | (interactive (list (magithub-interactive-issue))) 591 | (magithub-issue--browse issue)) 592 | 593 | (defun magithub-issue-visit (issue) 594 | "Visits ISSUE in Emacs. 595 | Interactively, this finds the issue at point." 596 | (interactive (list (magithub-interactive-issue))) 597 | (magithub-issue-view issue)) 598 | 599 | (defun magithub-pull-browse (pr) 600 | "Visits PR in the browser. 601 | Interactively, this finds the pull request at point." 602 | (interactive (list (magithub-interactive-pull-request))) 603 | (magithub-issue--browse pr)) 604 | 605 | (defun magithub-pull-visit (pr) 606 | "Visits PR in Emacs. 607 | Interactively, this finds the pull request at point." 608 | (interactive (list (magithub-interactive-pull-request))) 609 | (magithub-issue-view pr)) 610 | 611 | (defun magithub-issue--browse (issue-or-pr) 612 | "Visits ISSUE-OR-PR in the browser. 613 | Interactively, this finds the issue at point." 614 | (when-let ((url (alist-get 'html_url issue-or-pr))) 615 | (browse-url url))) 616 | 617 | (defun magithub-repolist-column-issue (_id) 618 | "Insert the number of open issues in this repository." 619 | (when (magithub-usable-p) 620 | (number-to-string (length (magithub-issues))))) 621 | 622 | (defun magithub-repolist-column-pull-request (_id) 623 | "Insert the number of open pull requests in this repository." 624 | (when (magithub-usable-p) 625 | (number-to-string (length (magithub-pull-requests))))) 626 | 627 | ;;; Pull Request handling 628 | 629 | 630 | (defun magithub-pull-request (repo number) 631 | "Retrieve a pull request in REPO by NUMBER." 632 | (magithub-cache :issues 633 | `(magithub-request 634 | (ghubp-get-repos-owner-repo-pulls-number 635 | ',repo '((number . ,number)))) 636 | :message 637 | (format "Getting pull request %s#%d..." 638 | (magithub-repo-name repo) 639 | number))) 640 | 641 | (defun magithub-remote-fork-p (remote) 642 | "True if REMOTE is a fork." 643 | (thread-last remote 644 | (magithub-repo-from-remote) 645 | (alist-get 'fork))) 646 | 647 | (defun magithub-pull-request-checked-out (pull-request) 648 | "True if PULL-REQUEST is currently checked out." 649 | (let-alist pull-request 650 | (let ((remote .user.login) 651 | (branch .head.ref)) 652 | (and (magit-remote-p remote) 653 | (magithub-remote-fork-p remote) 654 | (magit-branch-p branch) 655 | (string= remote (magit-get-push-remote branch)))))) 656 | 657 | ;; (make-obsolete 'magithub-pull-request-checkout 'magit-checkout-pull-request "0.1.6") 658 | ;; (defalias 'magithub-pull-request-checkout #'magit-checkout-pull-request) 659 | 660 | (provide 'magithub-issue) 661 | ;;; magithub-issue.el ends here 662 | -------------------------------------------------------------------------------- /magithub-label.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-labels.el --- -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | 7 | ;; This program is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation, either version 3 of the License, or 10 | ;; (at your option) any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; You should have received a copy of the GNU General Public License 18 | ;; along with this program. If not, see . 19 | 20 | ;;; Commentary: 21 | 22 | ;;; Code: 23 | 24 | (require 'thingatpt) 25 | (require 'ghub+) 26 | 27 | (require 'magithub-core) 28 | 29 | (defvar magit-magithub-label-section-map 30 | (let ((m (make-sparse-keymap))) 31 | (set-keymap-parent m magithub-map) 32 | (define-key m [remap magit-visit-thing] #'magithub-label-visit) 33 | (define-key m [remap magit-delete-thing] #'magithub-label-remove) 34 | (define-key m [remap magit-section-toggle] (lambda () (interactive))) 35 | (define-key m [remap magithub-browse-thing] #'magithub-label-browse) 36 | (define-key m [remap magithub-add-thing] #'magithub-label-add) 37 | m) 38 | "Keymap for label sections.") 39 | 40 | (defun magithub-label-list () 41 | "Return a list of issue and pull-request labels." 42 | (magithub-cache :label 43 | `(magithub-request 44 | (ghubp-unpaginate 45 | (ghubp-get-repos-owner-repo-labels 46 | ',(magithub-repo)))) 47 | :message 48 | "Loading labels...")) 49 | 50 | (defun magithub-label-read-labels (prompt &optional default) 51 | "Read some issue labels and return a list of strings. 52 | Available issues are provided by `magithub-label-list'. 53 | 54 | DEFAULT is a list of pre-selected labels. These labels are not 55 | prompted for again." 56 | (let ((remaining-labels 57 | (cl-set-difference (magithub-label-list) default 58 | :test (lambda (a b) 59 | (= (alist-get 'name a) 60 | (alist-get 'name b)))))) 61 | (magithub--completing-read-multiple 62 | prompt remaining-labels 63 | (lambda (l) (alist-get 'name l))))) 64 | 65 | (defalias 'magithub-label-visit #'magithub-label-browse) 66 | (defun magithub-label-browse (label) 67 | "Visit LABEL with `browse-url'. 68 | In the future, this will likely be replaced with a search on 69 | issues and pull requests with the label LABEL." 70 | (interactive (list (thing-at-point 'github-label))) 71 | (unless label 72 | (user-error "No label found at point to browse")) 73 | (unless (string= (ghubp-host) ghub-default-host) 74 | (user-error "Label browsing not yet supported on GitHub Enterprise; pull requests welcome!")) 75 | (let-alist (magithub-repo) 76 | (browse-url (format "%s/%s/%s/labels/%s" 77 | (ghubp-base-html-url) 78 | .owner.login .name (alist-get 'name label))))) 79 | 80 | (defcustom magithub-label-color-replacement-alist nil 81 | "Make certain label colors easier to see. 82 | In your theme, you may find that certain colors are very 83 | difficult to see. Customize this list to map GitHub's label 84 | colors to their Emacs replacements." 85 | :group 'magithub 86 | :type '(alist :key-type color :value-type color)) 87 | 88 | (defun magithub-label--get-display-color (label) 89 | "Gets the display color for LABEL. 90 | Respects `magithub-label-color-replacement-alist'." 91 | (let ((original (concat "#" (alist-get 'color label)))) 92 | (if-let ((color (assoc-string original magithub-label-color-replacement-alist t))) 93 | (cdr color) 94 | original))) 95 | 96 | (defun magithub-label-propertize (label) 97 | "Propertize LABEL according to its color. 98 | The face used is dynamically calculated, but it always inherits 99 | from `magithub-label'. Customize that to affect all labels." 100 | (propertize (alist-get 'name label) 101 | 'face (list :foreground (magithub-label--get-display-color label) 102 | :inherit 'magithub-label))) 103 | 104 | (defun magithub-label-color-replace (label new-color) 105 | "For LABEL, define a NEW-COLOR to use in the buffer." 106 | (interactive 107 | (list (thing-at-point 'github-label) 108 | (magithub-core-color-completing-read "Replace label color: "))) 109 | (let ((label-color (concat "#" (alist-get 'color label)))) 110 | (if-let ((cell (assoc-string label-color magithub-label-color-replacement-alist))) 111 | (setcdr cell new-color) 112 | (push (cons label-color new-color) 113 | magithub-label-color-replacement-alist))) 114 | (when (magithub-confirm-no-error 'label-save-customized-colors) 115 | (customize-save-variable 'magithub-label-color-replacement-alist 116 | magithub-label-color-replacement-alist 117 | "Auto-saved by `magithub-label-color-replace'")) 118 | (when (derived-mode-p 'magit-status-mode) 119 | (magit-refresh))) 120 | 121 | (defun magithub-label--verify-manage () 122 | (or (magithub-repo-push-p) 123 | (user-error "You don't have permission to manage labels in this repository"))) 124 | 125 | (defun magithub-label-remove (issue label) 126 | "From ISSUE, remove LABEL." 127 | (interactive (and (magithub-label--verify-manage) 128 | (list (thing-at-point 'github-issue) 129 | (thing-at-point 'github-label)))) 130 | (unless issue 131 | (user-error "No issue here")) 132 | (unless label 133 | (user-error "No label here")) 134 | (let-alist label 135 | (magithub-confirm 'remove-label .name) 136 | (prog1 (magithub-request 137 | (ghubp-delete-repos-owner-repo-issues-number-labels-name 138 | (magithub-issue-repo issue) issue label)) 139 | (magithub-cache-without-cache :issues 140 | (magit-refresh-buffer))))) 141 | 142 | (defun magithub-label-add (issue labels) 143 | "To ISSUE, add LABELS." 144 | (interactive (list (thing-at-point 'github-issue) 145 | (magithub-label-read-labels "Add labels: "))) 146 | (if (not (and issue labels)) 147 | (user-error "No issue/labels") 148 | (magithub-confirm 'add-label 149 | (s-join "," (ghubp-get-in-all '(name) labels)) 150 | (magithub-repo-name (magithub-issue-repo issue)) 151 | (alist-get 'number issue)) 152 | (prog1 (magithub-request 153 | (ghubp-post-repos-owner-repo-issues-number-labels 154 | (magithub-issue-repo issue) issue labels)) 155 | (magithub-cache-without-cache :issues 156 | (magit-refresh))))) 157 | 158 | (defun magithub-label-insert (label) 159 | "Insert LABEL into the buffer. 160 | If you need to insert many labels, use 161 | `magithub-label-insert-list'." 162 | (magit-insert-section (magithub-label label) 163 | (insert (magithub-label-propertize label)))) 164 | 165 | (defun magithub-label-insert-list (label-list) 166 | "Insert LABEL-LIST intro the buffer." 167 | (if (null label-list) 168 | (magit-insert-section (magithub-label) 169 | (insert (propertize "none" 'face 'magit-dimmed))) 170 | (while label-list 171 | (magithub-label-insert (pop label-list)) 172 | (when label-list 173 | (insert " "))))) 174 | 175 | (provide 'magithub-label) 176 | ;;; magithub-labels.el ends here 177 | -------------------------------------------------------------------------------- /magithub-notification.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-notification.el --- notification handling -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: lisp 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; View and interact with notifications. 24 | 25 | ;;; Code: 26 | 27 | (require 'thingatpt) 28 | (require 'magit-section) 29 | 30 | (require 'magithub-core) 31 | 32 | (defvar magit-magithub-notification-section-map 33 | (let ((m (make-sparse-keymap))) 34 | (set-keymap-parent m magithub-map) 35 | (define-key m [remap magit-visit-thing] #'magithub-notification-visit) 36 | (define-key m [remap magithub-browse-thing] #'magithub-notification-browse) 37 | (define-key m [remap magit-refresh] #'magithub-notification-refresh) 38 | m)) 39 | 40 | (defvar magit-magithub-notifications-section-map 41 | (let ((m (make-sparse-keymap))) 42 | (set-keymap-parent m magithub-map) 43 | (define-key m [remap magit-refresh] #'magithub-notification-refresh) 44 | m)) 45 | 46 | (defun magithub-notifications (&optional include-read only-participating since before) 47 | "Get notifications for the currently-authenticated user. 48 | If INCLUDE-READ is non-nil, read notifications are returned as 49 | well. 50 | 51 | If ONLY-PARTICIPATING is non-nil, only return notifications that 52 | the user is directly participating in. 53 | 54 | If SINCE/BEFORE are non-nil, they are time values. Only 55 | notifications received since/before this value will be returned. 56 | See also Info node `(elisp)Time of Day'." 57 | (let (args) 58 | (when include-read 59 | (push '(:all "true") args)) 60 | (when only-participating 61 | (push '(:participating "true") args)) 62 | (when since 63 | (push `(:since ,(format-time-string "%FT%T%z" since)) args)) 64 | (when before 65 | (push `(:before ,(format-time-string "%FT%T%z" before)) args)) 66 | (magithub-cache :notification 67 | `(magithub-request 68 | (ghubp-unpaginate 69 | (ghubp-get-notifications ,@(apply #'append args))))))) 70 | 71 | (defun magithub-notification-refresh () 72 | (interactive) 73 | (magithub-cache-without-cache :notification 74 | (magit-refresh)) 75 | (message "(magithub) notifications refreshed")) 76 | 77 | (defun magithub-notification-read-p (notification) 78 | "Non-nil if NOTIFICATION has been read." 79 | (not (magithub-notification-unread-p notification))) 80 | 81 | (defun magithub-notification-unread-p (notification) 82 | "Non-nil if NOTIFICATION has been not been read." 83 | (alist-get 'unread notification)) 84 | 85 | (defconst magithub-notification-reasons 86 | '(("assign" . "You were assigned to the Issue.") 87 | ("author" . "You created the thread.") 88 | ("comment" . "You commented on the thread.") 89 | ("invitation" . "You accepted an invitation to contribute to the repository.") 90 | ("manual" . "You subscribed to the thread (via an Issue or Pull Request).") 91 | ("mention" . "You were specifically @mentioned in the content.") 92 | ("state_change" . "You changed the thread state (for example, closing an Issue or merging a Pull Request).") 93 | ("subscribed" . "You're watching the repository.") 94 | ("team_mention" . "You were on a team that was mentioned.")) 95 | "Human-readable description of possible notification reasons. 96 | Stripped from the GitHub API Docs: 97 | 98 | URL `https://developer.github.com/v3/activity/notifications/#notification-reasons'.") 99 | 100 | (defun magithub-notification-reason (notification &optional expanded) 101 | "Get the reason NOTIFICATION exists. 102 | If EXPANDED is non-nil, use `magithub-notification-reasons' to 103 | get a more verbose explanation." 104 | (let-alist notification 105 | (if expanded 106 | (cdr (assoc-string .reason magithub-notification-reasons 107 | "(Unknown)")) 108 | .reason))) 109 | 110 | (declare-function magithub-issue-view "magithub-issue-view" (issue)) 111 | (defalias 'magithub-notification-visit #'magithub-notification-browse) 112 | (defun magithub-notification-browse (notification) 113 | "Visits the URL pointed to by NOTIFICATION." 114 | (interactive (list (thing-at-point 'github-notification))) 115 | (magithub-request 116 | (if notification 117 | (let-alist notification 118 | (cond 119 | ((member .subject.type '("Issue" "PullRequest")) 120 | (ghubp-patch-notifications-threads-id notification) 121 | (require 'magithub-issue-view) 122 | (magithub-issue-view (ghubp-follow-get .subject.url))) 123 | (t (if-let ((url (or .subject.latest_comment_url .subject.url)) 124 | (html-url (alist-get 'html_url (ghubp-follow-get url)))) 125 | (browse-url html-url) 126 | (user-error "No target URL found"))))) 127 | (user-error "No notification here")))) 128 | 129 | (defvar magithub-notification-details-hook 130 | '(magithub-notification-detail-insert-type 131 | magithub-notification-detail-insert-updated 132 | magithub-notification-detail-insert-expanded-reason) 133 | "Detail functions for notification-type sections. 134 | These details appear under notifications as expandable content. 135 | 136 | Each function takes the notification object as its only 137 | argument.") 138 | 139 | (defun magithub-notification-insert-section (notification) 140 | "Insert NOTIFICATION as a section into the buffer." 141 | (let-alist notification 142 | (magit-insert-section (magithub-notification notification (not .unread)) 143 | (magit-insert-heading 144 | (format "%-12s %s" 145 | (propertize (magithub-notification-reason notification) 146 | 'face 'magithub-notification-reason 147 | 'help-echo (magithub-notification-reason notification t)) 148 | (propertize (concat .subject.title "\n") 149 | 'face (if .unread 'highlight)))) 150 | (run-hook-with-args 'magithub-notification-details-hook notification)))) 151 | 152 | (defun magithub-notification-detail-insert-type (notification) 153 | "Insert NOTIFICATION's type." 154 | (let-alist notification 155 | (insert (format "%-12s %s\n" "Type:" 156 | (propertize .subject.type 'face 'magit-dimmed))))) 157 | 158 | (defun magithub-notification-detail-insert-updated (notification) 159 | "Insert a timestamp of when NOTIFICATION was last updated." 160 | (let-alist notification 161 | (insert (format "%-12s %s\n" "Updated:" 162 | (propertize .updated_at 'face 'magit-dimmed))))) 163 | 164 | (defun magithub-notification-detail-insert-expanded-reason (notification) 165 | "Insert NOTIFICATION's expanded reason. 166 | See also `magithub-notification-reasons'." 167 | (insert (format "%-12s %s\n" "Reason:" 168 | (propertize (or (magithub-notification-reason notification t) 169 | "(no description available)") 170 | 'face 'magit-dimmed)))) 171 | 172 | (provide 'magithub-notification) 173 | ;;; magithub-notification.el ends here 174 | -------------------------------------------------------------------------------- /magithub-orgs.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-orgs.el --- Organization handling -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: tools 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Utilities for dealing with organizations. 24 | 25 | ;;; Code: 26 | 27 | (require 'magithub-core) 28 | 29 | (defun magithub-orgs-list () 30 | "List organizations for the currently authenticated user." 31 | (magithub-cache :user-demographics 32 | `(magithub-request 33 | (ghubp-get-user-orgs)))) 34 | 35 | (provide 'magithub-orgs) 36 | ;;; magithub-orgs.el ends here 37 | -------------------------------------------------------------------------------- /magithub-repo.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-repo.el --- repo tools -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: lisp 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Basic tools for working with repositories. 24 | 25 | ;;; Code: 26 | 27 | (require 'magit) 28 | (require 'thingatpt) 29 | 30 | (require 'magithub-core) 31 | 32 | (defvar magit-magithub-repo-section-map 33 | (let ((m (make-sparse-keymap))) 34 | (set-keymap-parent m magithub-map) 35 | (define-key m [remap magithub-browse-thing] #'magithub-repo-browse) 36 | m)) 37 | 38 | (defun magithub-repo-browse (repo) 39 | (interactive (list (thing-at-point 'github-repo))) 40 | (unless repo 41 | (user-error "No repository found at point")) 42 | (let-alist repo 43 | (browse-url .html_url))) 44 | 45 | (defun magithub-repo-data-dir (repo) 46 | (let-alist repo 47 | (expand-file-name (format "%s/%s/" .owner.login .name) 48 | magithub-dir))) 49 | 50 | (provide 'magithub-repo) 51 | ;;; magithub-repo.el ends here 52 | -------------------------------------------------------------------------------- /magithub-settings.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-settings.el --- repo-specific user settings -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: tools 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;;; Code: 24 | 25 | (require 'magit) 26 | 27 | (defconst magithub-settings-section "magithub" 28 | "This string prefixes all Magithub-related git settings.") 29 | (defconst magithub-settings-prefix "magithub" 30 | "This string prefixes all Magithub-related git settings.") 31 | 32 | (defmacro magithub-settings--simple (popup key variable docstring choices default) 33 | (declare (indent 3) (doc-string 4)) 34 | (unless (stringp variable) 35 | (error "VARIABLE must be a string: %S" variable)) 36 | (let* ((variable (concat magithub-settings-section "." variable)) 37 | (Nset (concat "magithub-settings--set-" variable)) 38 | (Nfmt (concat "magithub-settings--format-" variable))) 39 | (let ((Sset (intern Nset)) 40 | (docstring (format "%s\n\nThis is the Git variable %S." docstring variable))) 41 | `(progn 42 | (transient-define-infix ,Sset () ,docstring 43 | :class 'magit--git-variable:choices 44 | :variable ,variable 45 | :choices ,choices 46 | :default ,default) 47 | (transient-define-infix ,(intern Nfmt) () ,(format "See `%s'." Nset) 48 | :class 'magit--git-variable:choices 49 | :variable ,variable 50 | :choices ,choices 51 | :default ,default) 52 | (transient-append-suffix ',popup "h" 53 | '(,key ,variable ,Sset)) 54 | ,variable)))) 55 | 56 | (defun magithub-settings--value-or (variable default &optional accessor) 57 | (declare (indent 2)) 58 | (if (magit-get variable) 59 | (funcall (or accessor #'magit-get) variable) 60 | default)) 61 | 62 | ;;;###autoload (autoload 'magithub-settings-popup "magithub-settings" nil t) 63 | (transient-define-prefix magithub-settings-popup () 64 | "Popup console for managing Magithub settings." 65 | ["test" 66 | ("h" "Ask for help on Gitter" magithub--meta-help)] 67 | ) 68 | 69 | (magithub-settings--simple magithub-settings-popup "e" "enabled" 70 | "Enable/disable all Magithub functionality." 71 | '("true" "false") "true") 72 | 73 | (defun magithub-enabled-p () 74 | "Returns non-nil if Magithub content is available." 75 | (magithub-settings--value-or "magithub.enabled" t 76 | #'magit-get-boolean)) 77 | 78 | (magithub-settings--simple magithub-settings-popup "o" "online" 79 | "Controls whether Magithub is online or offline. 80 | 81 | - `true': requests are made to GitHub for missing data 82 | - `false': no requests are made to GitHub 83 | 84 | In both cases, when there is data in the cache, that data is 85 | used. Refresh the buffer with a prefix argument to disregard the 86 | cache while refreshing: \\\\[universal-argument] \\[magit-refresh]" 87 | '("true" "false") "true") 88 | 89 | (defun magithub-online-p () 90 | "See `magithub-settings--set-magithub.online'. 91 | Returns the value as t or nil." 92 | (magithub-settings--value-or "magithub.online" t 93 | #'magit-get-boolean)) 94 | 95 | 96 | (magithub-settings--simple magithub-settings-popup "s" "status.includeStatusHeader" 97 | "When true, the project status header is included in 98 | `magit-status-headers-hook'." 99 | '("true" "false") "true") 100 | 101 | (defun magithub-settings-include-status-p () 102 | "Non-nil if the project status header should be included." 103 | (magithub-settings--value-or "magithub.status.includeStatusHeader" t 104 | #'magit-get-boolean)) 105 | 106 | 107 | (magithub-settings--simple magithub-settings-popup "i" "status.includeIssuesSection" 108 | "When true, project issues are included in 109 | `magit-status-sections-hook'." 110 | '("true" "false") "true") 111 | 112 | (defun magithub-settings-include-issues-p () 113 | "Non-nil if the issues section should be included." 114 | (magithub-settings--value-or "magithub.status.includeIssuesSection" t 115 | #'magit-get-boolean)) 116 | 117 | 118 | (magithub-settings--simple magithub-settings-popup "p" "status.includePullRequestsSection" 119 | "When true, project pull requests are included in 120 | `magit-status-sections-hook'." 121 | '("true" "false") "true") 122 | 123 | (defun magithub-settings-include-pull-requests-p () 124 | "Non-nil if the pull requests section should be included." 125 | (magithub-settings--value-or "magithub.status.includePullRequestsSection" t 126 | #'magit-get-boolean)) 127 | 128 | 129 | (magithub-settings--simple magithub-settings-popup "x" "contextRemote" 130 | "Use REMOTE as the proxy. 131 | When set, the proxy is used whenever a GitHub repository is needed." 132 | (magit-list-remotes) "origin") 133 | 134 | (defun magithub-settings-context-remote () 135 | "Determine the correct remote to use for issue-tracking." 136 | (magithub-settings--value-or "magithub.contextRemote" "origin")) 137 | 138 | (defvar magithub-confirmation 139 | ;; todo: future enhancement - could allow prompt message to be a function. 140 | '((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?") 141 | (submit-pr long "Are you sure you want to submit this pull request?") 142 | (submit-pr-from-issue long "Are you sure you wish to create a PR based on %s by merging `%s' into `%s'?") 143 | (pr-allow-maintainers-to-submit short "Allow maintainers to modify this pull request?") 144 | (submit-issue long "Are you sure you want to submit this issue?") 145 | (remove-label short "Remove label {%s} from this issue?") 146 | (add-label short "Add label(s) {%s} to %s#%s?") 147 | (create-repo-as-private long "Will this be a private repository?") 148 | (init-repo-after-create short "Not inside a Git repository; initialize one here?") 149 | (fork long "Fork this repository?") 150 | (fork-create-spinoff short "Create a spinoff branch?") 151 | (fork-add-me-as-remote short "Add %s as a remote in this repository?") 152 | (fork-set-upstream-to-me short "Set upstream to %s?") 153 | (clone long "Clone %s to %s?") 154 | (clone-fork-set-upstream-to-parent short "This repository appears to be a fork of %s; set upstream to that remote?") 155 | (clone-fork-set-proxy-to-upstream short "Use upstream as a proxy for issues, etc.?") 156 | (clone-open-magit-status short "%s/%s has finished cloning to %s. Open?") 157 | (clone-create-directory short "%s does not exist. Create it?") 158 | (ci-refresh-when-offline short "Magithub offline; refresh statuses anyway?") 159 | (refresh short "Refresh GitHub data?") 160 | (refresh-when-API-unresponsive short "GitHub doesn't seem to be responding, are you sure?") 161 | (label-save-customized-colors short "Save customization?") 162 | (user-email short "Email @%s at \"%s\"?") 163 | (user-email-self short "Email yourself?") 164 | (assignee-add long "Assign '%s' to %s#%d?") 165 | (assignee-remove long "Remove '%s' from %s#%d?") 166 | (comment short "Submit this comment to %s?") 167 | (comment-edit short "Commit this edit?") 168 | (comment-delete long "Are you sure you wish to delete this comment?") 169 | (report-error short "%s Report? (A bug report will be placed in your clipboard.)") 170 | (issue-reopen short "Reopen %s?") 171 | (issue-close short "Close %s?")) 172 | "Alist of actions/decisions to their default behaviors and associated prompts. 173 | 174 | These behaviors can be overridden with (man)git-config. 175 | 176 | A behavior is one of the following symbols: 177 | 178 | `long' 179 | use `yes-or-no-p' to confirm each time 180 | 181 | `short' 182 | use `y-or-n-p' to confirm each time 183 | 184 | `allow' 185 | always allow action 186 | 187 | `deny' 188 | always deny action") 189 | 190 | (defun magithub-confirm (action &rest prompt-format-args) 191 | "Confirm ACTION using Git config settings. 192 | See `magithub--confirm'." 193 | (magithub--confirm action prompt-format-args nil)) 194 | 195 | (defun magithub-confirm-no-error (action &rest prompt-format-args) 196 | "Confirm ACTION using Git config settings. 197 | See `magithub--confirm'." 198 | (magithub--confirm action prompt-format-args t)) 199 | 200 | (defun magithub-settings--from-confirmation-action (action) 201 | "Create a magithub.confirm.* setting from ACTION." 202 | (concat 203 | magithub-settings-section 204 | ".confirm." 205 | (let ((pascal-case (replace-regexp-in-string "-" "" (upcase-initials (symbol-name action))))) 206 | ;; we have PascalCase, we want camelCase 207 | (concat (downcase (substring pascal-case 0 1)) 208 | (substring pascal-case 1))))) 209 | 210 | (defvar magithub-confirm-y-or-n-p-map 211 | (let ((m (make-keymap))) 212 | (define-key m (kbd "C-g") 'quit) ;don't know how to remap keyboard-quit here 213 | (define-key m "q" 'quit) 214 | (define-key m (kbd "C-u") 'cycle) 215 | (define-key m "y" 'allow) 216 | (define-key m "n" 'deny) 217 | m)) 218 | 219 | (defvar magithub-confirm-yes-or-no-p-map 220 | (let ((m (make-sparse-keymap))) 221 | (set-keymap-parent m minibuffer-local-map) 222 | (define-key m [remap universal-argument] #'magithub--confirm-cycle-set-default-interactive) 223 | m)) 224 | 225 | (defvar magithub-confirm--current-cycle nil 226 | "Control how a response should be saved. 227 | This variable should never be set globally; always let-bind it! 228 | 229 | nil 230 | Do not save the response 231 | 232 | `local' 233 | Save response locally 234 | 235 | `global' 236 | Save response globally") 237 | 238 | (defun magithub-confirm-yes-or-no-p (prompt var) 239 | "Like `yes-or-no-p', but optionally save response to VAR." 240 | (let ((p (concat prompt (substitute-command-keys " (yes, no, or \\[universal-argument]*) "))) 241 | magithub-confirm--current-cycle old-cycle done answer changed) 242 | (while (not done) 243 | (setq changed (not (eq old-cycle magithub-confirm--current-cycle)) 244 | old-cycle magithub-confirm--current-cycle 245 | answer (read-from-minibuffer 246 | (magithub--confirm-get-prompt-with-cycle 247 | p var magithub-confirm--current-cycle) 248 | ;; default in what was already entered if the save-behavior changed 249 | (when changed answer) 250 | magithub-confirm-yes-or-no-p-map nil 251 | 'yes-or-no-p-history)) 252 | ;; If the user activated `magithub--confirm-cycle-set-default-interactive', 253 | ;; `magithub-confirm--current-cycle' will have been updated. 254 | (when (and (eq old-cycle magithub-confirm--current-cycle) 255 | (stringp answer)) 256 | (setq answer (downcase (s-trim answer))) 257 | (if (member answer '("yes" "no")) 258 | (setq done t) 259 | (message "Please answer yes or no. ") 260 | (sleep-for 2)))) 261 | (when magithub-confirm--current-cycle 262 | (magithub--confirm-cycle-save-var-value 263 | var (pcase answer 264 | ("yes" "allow") 265 | ("no" "deny")))) 266 | (string= answer "yes"))) 267 | 268 | (defun magithub-confirm-y-or-n-p (prompt var) 269 | "Like `y-or-n-p', but optionally save response to VAR." 270 | (let ((cursor-in-echo-area t) 271 | (newprompt (format "%s (y, n, C-u*) " prompt)) 272 | magithub-confirm--current-cycle done answer varval explain) 273 | (while (not done) 274 | (setq newprompt 275 | (if explain 276 | (format "%s (please answer y or n or use C-u to cycle through and set default answers) " prompt) 277 | (format "%s (y, n, C-u*) " prompt)) 278 | explain nil 279 | answer 280 | (lookup-key magithub-confirm-y-or-n-p-map 281 | (vector 282 | (read-key (magithub--confirm-get-prompt-with-cycle 283 | newprompt var magithub-confirm--current-cycle))))) 284 | (pcase answer 285 | (`quit (keyboard-quit)) 286 | (`cycle (magithub--confirm-cycle-set-default)) 287 | (`allow (setq done t varval "allow")) 288 | (`deny (setq done t varval "deny")) 289 | (_ (setq explain t)))) 290 | (when (stringp varval) 291 | (magithub--confirm-cycle-save-var-value var varval)) 292 | (eq answer 'allow))) 293 | 294 | (defun magithub--confirm-cycle-save-var-value (var val) 295 | "Save VAR with VAL locally or globally. 296 | See `magithub-confirm--current-cycle'." 297 | (pcase magithub-confirm--current-cycle 298 | (`local (magit-set val var)) 299 | (`global (magit-set val "--global" var)))) 300 | 301 | (defun magithub--confirm-cycle-set-default-interactive () 302 | "In `magithub--confirm-yes-or-no-p', update behavior." 303 | (interactive) 304 | (magithub--confirm-cycle-set-default) 305 | (exit-minibuffer)) 306 | 307 | (defun magithub--confirm-cycle-set-default () 308 | (setq magithub-confirm--current-cycle 309 | (cadr (member magithub-confirm--current-cycle 310 | '(nil local global))))) 311 | 312 | (defun magithub--confirm-get-prompt-with-cycle (prompt var cycle) 313 | "Get an appropriate PROMPT associated with VAR for CYCLE. 314 | See `magithub-confirm--current-cycle'." 315 | (propertize 316 | (pcase cycle 317 | (`local (format "%s[and don't ask again: git config %s] " prompt var)) 318 | (`global (format "%s[and don't ask again: git config --global %s] " prompt var)) 319 | (_ prompt)) 320 | 'face 'minibuffer-prompt)) 321 | 322 | (defun magithub--confirm (action prompt-format-args noerror) 323 | "Confirm ACTION using Git config settings. 324 | 325 | When PROMPT-FORMAT-ARGS is non-nil, the prompt piece of ACTION's 326 | confirmation spec is passed through `format' with these 327 | arguments. 328 | 329 | Unless NOERROR is non-nil, denying ACTION will result in a user 330 | error to abort the action. 331 | 332 | This is like `magit-confirm', but a little more powerful. It 333 | might belong in Magit, but we'll see how it goes." 334 | (let ((spec (alist-get action magithub-confirmation)) 335 | var default prompt setting choice) 336 | (unless spec 337 | (magithub-error "No confirmation settings for %S" spec)) 338 | (unless (= 2 (length spec)) 339 | (magithub-error "Spec for %S must have 2 members: %S" action spec)) 340 | (setq default (symbol-name (nth 0 spec)) 341 | prompt (nth 1 spec) 342 | var (magithub-settings--from-confirmation-action action)) 343 | (when prompt-format-args 344 | (setq prompt (apply #'format prompt prompt-format-args))) 345 | (when (and (null noerror) (string= "deny" default)) 346 | (magithub-error (format "The default for %S is deny, but this will cause an error" action))) 347 | 348 | (setq setting (magithub-settings--value-or var default)) 349 | (when (and (string= setting "deny") 350 | (null noerror)) 351 | (let ((raw (magit-git-string "config" "--show-origin" var)) 352 | washed) 353 | (when (string-match (rx bos (group (+ any)) (+ space) (group (+ any)) eos) raw) 354 | (setq washed (format "%s => %s" 355 | (match-string 1 raw) 356 | (match-string 2 raw)))) 357 | (user-error "Abort per %s [%s]" var (or washed raw)))) 358 | 359 | (setq choice 360 | (pcase setting 361 | ("long" (magithub-confirm-yes-or-no-p prompt var)) 362 | ("short" (magithub-confirm-y-or-n-p prompt var)) 363 | ("allow" t) 364 | ("deny" nil))) 365 | 366 | (or choice 367 | (unless noerror 368 | (user-error "Abort"))))) 369 | 370 | (defun magithub-confirm-set-default-behavior (action default &optional globally) 371 | "Set the default behavior of ACTION to DEFAULT. 372 | 373 | If GLOBALLY is non-nil, make this configuration apply globally. 374 | 375 | See `magithub-confirmation' for valid values of DEFAULT." 376 | (unless (alist-get action magithub-confirmation) 377 | (error "Action not defined: %S" action)) 378 | (let* ((var (magithub-settings--from-confirmation-action action)) 379 | (args (list var))) 380 | (when globally 381 | (push "--global" args)) 382 | (apply #'magit-set 383 | (if (memq default '(long short allow deny)) 384 | (symbol-name default) 385 | (error "Invalid default behavior: %S" default)) 386 | args) 387 | default)) 388 | 389 | (provide 'magithub-settings) 390 | ;;; magithub-settings.el ends here 391 | -------------------------------------------------------------------------------- /magithub-user.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-user.el --- Inspect users -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: lisp 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Code for dealing with the current user and other users. 24 | 25 | ;;; Code: 26 | 27 | (require 'ghub+) 28 | (require 'cl-lib) 29 | (require 'thingatpt) 30 | 31 | (require 'magithub-core) 32 | 33 | (defvar magit-magithub-user-section-map 34 | (let ((m (make-sparse-keymap))) 35 | (set-keymap-parent m magithub-map) 36 | (define-key m [remap magit-visit-thing] #'magithub-user-visit) 37 | (define-key m [remap magithub-browse-thing] #'magithub-user-browse) 38 | (define-key m "m" #'magithub-user-email) 39 | m)) 40 | 41 | (defvar magit-magithub-assignee-section-map 42 | (let ((m (make-sparse-keymap))) 43 | (set-keymap-parent m magit-magithub-user-section-map) 44 | (define-key m "a" #'magithub-assignee-add) 45 | (define-key m [remap magit-delete-thing] #'magithub-assignee-remove) 46 | m)) 47 | 48 | (defun magithub-user-me () 49 | "Return the currently-authenticated user." 50 | (magithub-cache :user-demographics 51 | `(magithub-request 52 | (ghubp-get-user)) 53 | :message 54 | "user object for the currently-authenticated user")) 55 | 56 | (defun magithub-user (user) 57 | "Return the full object for USER." 58 | (magithub-cache :user-demographics 59 | `(magithub-request 60 | (ghubp-get-users-username ',user)))) 61 | 62 | (defun magithub-assignee--verify-manage () 63 | (or (magithub-repo-push-p) 64 | (user-error "You don't have permission to manage assignees in this repository"))) 65 | 66 | (defun magithub-assignee-add (issue user) 67 | (interactive (when (magithub-assignee--verify-manage) 68 | (let ((issue (magit-section-parent-value (magit-current-section)))) 69 | (list issue 70 | (magithub-user-choose-assignee 71 | "Choose an assignee: " 72 | (magithub-issue-repo issue)))))) 73 | (let-alist `((repo . ,(magithub-issue-repo issue)) 74 | (issue . ,issue) 75 | (user . ,user)) 76 | (magithub-confirm 'assignee-add 77 | .user.login 78 | (magithub-repo-name .repo) 79 | .issue.number) 80 | (prog1 (magithub-request 81 | (ghubp-post-repos-owner-repo-issues-number-assignees 82 | .repo .issue (list .user))) 83 | (let ((sec (magit-current-section))) 84 | (magithub-cache-without-cache :issues 85 | (magit-refresh-buffer)) 86 | (magit-section-show sec))))) 87 | 88 | (defun magithub-assignee-remove (issue user) 89 | (interactive (when (magithub-assignee--verify-manage) 90 | (list (thing-at-point 'github-issue) 91 | (thing-at-point 'github-user)))) 92 | (let-alist `((repo . ,(magithub-issue-repo issue)) 93 | (issue . ,issue) 94 | (user . ,user)) 95 | (magithub-confirm .user.login 96 | (magithub-repo-name .repo) 97 | .issue.number) 98 | (prog1 (magithub-request 99 | (ghubp-delete-repos-owner-repo-issues-number-assignees .repo .issue (list .user))) 100 | (magithub-cache-without-cache :issues 101 | (magit-refresh-buffer))))) 102 | 103 | (defun magithub-user-choose (prompt &optional default-user) 104 | (let (ret-user new-username) 105 | (while (not ret-user) 106 | (setq new-username 107 | (magit-read-string-ns 108 | (concat prompt 109 | (if new-username (format " ['%s' not found]" new-username))) 110 | (alist-get 'login default-user))) 111 | (when-let ((try (condition-case _ 112 | (magithub-request 113 | (ghubp-get-users-username `((login . ,new-username)))) 114 | (ghub-404 nil)))) 115 | (setq ret-user try))) 116 | ret-user)) 117 | 118 | (defun magithub-user-choose-assignee (prompt &optional repo default-user) 119 | (magithub--completing-read 120 | prompt 121 | (magithub-request 122 | (ghubp-get-repos-owner-repo-assignees repo)) 123 | (lambda (user) (let-alist user .login)) 124 | nil t default-user)) 125 | 126 | (defalias 'magithub-user-visit #'magithub-user-browse) 127 | (defun magithub-user-browse (user) 128 | "Open USER on GitHub." 129 | (interactive (list (thing-at-point 'github-user))) 130 | (if user 131 | (browse-url (alist-get 'html_url user)) 132 | (user-error "No user here"))) 133 | 134 | (defun magithub-user-email (user) 135 | "Email USER." 136 | (interactive (list (thing-at-point 'github-user))) 137 | (when (string= (alist-get 'login (magithub-user-me)) 138 | (alist-get 'login user)) 139 | (magithub-confirm 'user-email-self)) 140 | (unless user 141 | (user-error "No user here")) 142 | (let-alist user 143 | (unless .email 144 | (user-error "No email found; target user may be private")) 145 | (magithub-confirm 'user-email .login .email) 146 | (browse-url (format "mailto:%s" .email)))) 147 | 148 | (provide 'magithub-user) 149 | ;;; magithub-user.el ends here 150 | -------------------------------------------------------------------------------- /magithub.el: -------------------------------------------------------------------------------- 1 | ;;; magithub.el --- Magit interfaces for GitHub -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: git, tools, vc 7 | ;; Homepage: https://github.com/vermiculus/magithub 8 | ;; Package-Requires: ((emacs "25") (magit "2.12") (s "1.12.0") (ghub+ "0.3") (git-commit "2.12") (markdown-mode "2.3")) 9 | ;; Package-Version: 0.1.7 10 | 11 | ;; This program is free software; you can redistribute it and/or modify 12 | ;; it under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation, either version 3 of the License, or 14 | ;; (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program. If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; Magithub is a Magit-based interface to GitHub. 27 | ;; 28 | ;; Integrated into Magit workflows, Magithub lets you interact with 29 | ;; your GitHub repositories and manage your work/play from emacs: 30 | ;; 31 | ;; - push brand-new local repositories up to GitHub 32 | ;; - create forks of existing repositories 33 | ;; - submit pull requests upstream 34 | ;; - view and create issues 35 | ;; - view, create, and edit comments 36 | ;; - view status checks (e.g., Travis CI) 37 | ;; - manage labels and assignees 38 | ;; - view/visit notifications 39 | ;; - write personal notes on issues for reference later 40 | ;; - and probably more... 41 | ;; 42 | ;; Press `H' in the status buffer to get started -- happy hacking! 43 | 44 | ;;; Code: 45 | 46 | (require 'magit) 47 | (require 'magit-process) 48 | (require 'cl-lib) 49 | (require 's) 50 | (require 'dash) 51 | (require 'ghub+) 52 | 53 | (require 'magithub-core) 54 | (require 'magithub-issue) 55 | (require 'magithub-ci) 56 | (require 'magithub-issue-post) 57 | (require 'magithub-issue-tricks) 58 | (require 'magithub-orgs) 59 | (require 'magithub-dash) 60 | 61 | ;;;###autoload (autoload 'magithub-dispatch-popup "magithub" nil t) 62 | (transient-define-prefix magithub-dispatch-popup () 63 | "Popup console for dispatching other Magithub popups." 64 | [["Variables" 65 | ("C" "Settings..." magithub-settings-popup)] 66 | ["Actions" 67 | ("d" "Dashboard" magithub-dashboard) 68 | ("H" "Browse on GitHub" magithub-browse) 69 | ("c" "Create on GitHub" magithub-create) 70 | ("f" "Fork this repo" magithub-fork) 71 | ("i" "Submit an issue" magithub-issue-new) 72 | ("p" "Submit a pull request" magithub-pull-request-new)] 73 | ["Meta" 74 | ("&" "Request a feature or report a bug" magithub--meta-new-issue) 75 | ("h" "Ask for help on Gitter" magithub--meta-help)]] 76 | ) 77 | 78 | ;;;###autoload 79 | (eval-after-load 'magit 80 | '(progn 81 | (require 'transient) 82 | (when (functionp 'magit-am) 83 | (transient-append-suffix 'magit-dispatch "C-h m" 84 | '("H" "Magithub" magithub-dispatch-popup))) 85 | (define-key magit-status-mode-map 86 | "H" #'magithub-dispatch-popup))) 87 | 88 | (defun magithub-browse () 89 | "Open the repository in your browser." 90 | (interactive) 91 | (unless (magithub-github-repository-p) 92 | (user-error "Not a GitHub repository")) 93 | (magithub-repo-visit (magithub-repo))) 94 | 95 | (defun magithub-browse-file (file &optional begin end use-default-branch) 96 | "Open FILE in your browser highlighting lines BEGIN to END. 97 | 98 | FILE is a path to relative to the root of the Git repository. 99 | 100 | If FILE and BEGIN/END are not provided, they are detected from 101 | the current context: 102 | 103 | 1. In a file-visiting buffer, the buffer's file context and 104 | active region are used. 105 | 106 | 2. In a dired- or magit-like buffer, the file at point is used. 107 | 108 | If USE-DEFAULT-BRANCH is set (interactively, via prefix 109 | argument), then browse the file at the default branch of the 110 | repository instead of the current HEAD." 111 | (interactive (list nil nil nil current-prefix-arg)) 112 | (magithub-browse-file--url-fn-interactive #'browse-url 113 | file begin end use-default-branch)) 114 | 115 | (defun magithub-browse-file-copy-location-as-kill (file &optional begin end use-default-branch) 116 | "Like `magithub-browse-file', but copy the URL as a kill instead." 117 | (interactive (list nil nil nil current-prefix-arg)) 118 | (magithub-browse-file--url-fn-interactive #'kill-new 119 | file begin end use-default-branch)) 120 | 121 | (defun magithub-browse-file--url-fn-interactive (func file begin end use-default-branch) 122 | "Provides boilerplate for using `magithub-browse-file--url'." 123 | (declare (indent 1)) 124 | (let* ((args (magithub-browse-file--get-file-and-region file begin end)) 125 | (file (plist-get args :file)) 126 | (begin (plist-get args :begin)) 127 | (end (plist-get args :end))) 128 | (unless file 129 | (user-error "Could not detect a file at point")) 130 | (let ((default-directory (if (file-directory-p file) 131 | file 132 | (file-name-directory file)))) 133 | (unless (magithub-github-repository-p) 134 | (user-error "Not a GitHub repository")) 135 | (funcall func (magithub-browse-file--url 136 | file begin end use-default-branch))))) 137 | 138 | (defun magithub-browse-file--url (file begin end use-default-branch) 139 | "Wrapper for `magithub-browse-file--url2' providing sensible defaults." 140 | (magithub-browse-file--url2 141 | (magithub-repo) (magit-toplevel) file 142 | (or (and use-default-branch 'default-branch) 143 | (magit-rev-parse "HEAD")) 144 | begin end)) 145 | 146 | (defun magithub-browse-file--url2 (repo toplevel file rev begin end) 147 | "For REPO cloned at TOPLEVEL, calculate the URL for FILE at REV. 148 | If provided, the region from lines BEGIN and END will be highlighted." 149 | (let-alist repo 150 | (setq file (string-remove-prefix toplevel file)) 151 | (if (eq rev 'default-branch) 152 | (setq rev .default_branch)) 153 | (if (string-empty-p file) 154 | (format "%s/tree/%s" .html_url rev) 155 | (format "%s/blob/%s/%s%s" .html_url rev file 156 | (or (magithub-browse-file--get-anchor begin end) ""))))) 157 | 158 | (defun magithub-browse-file--get-file-and-region (file begin end) 159 | "Get an appropriate file at point. 160 | FILE, BEGIN, and END are override values." 161 | (let ((region-active-p (region-active-p))) 162 | (list :file 163 | (expand-file-name 164 | (or file 165 | buffer-file-name 166 | (and (derived-mode-p 'dired-mode) 167 | (or (dired-file-name-at-point) 168 | default-directory)) 169 | (and (derived-mode-p 'magit-status-mode) 170 | (magit-file-at-point)))) 171 | :begin 172 | (or begin 173 | (and buffer-file-name 174 | (line-number-at-pos 175 | (if region-active-p 176 | (region-beginning) 177 | (point))))) 178 | :end 179 | (or end 180 | (and buffer-file-name 181 | region-active-p 182 | (line-number-at-pos 183 | (region-end))))))) 184 | 185 | (defun magithub-browse-file--get-anchor (&optional begin end) 186 | (cond 187 | ((and begin end) 188 | (format "#L%d-L%d" begin end)) 189 | (begin 190 | (format "#L%d" begin)))) 191 | 192 | (defun magithub-browse-file-blame (file &optional begin end use-default-branch) 193 | "Blame FILE in the browser. 194 | 195 | If USE-DEFAULT-BRANCH is set (interactively, via prefix 196 | argument), then blame the file at the default branch of the 197 | repository instead of the current HEAD." 198 | (interactive (list nil current-prefix-arg)) 199 | (let* ((args (magithub-browse-file--get-file-and-region file begin end)) 200 | (file (plist-get args :file)) 201 | (begin (plist-get args :begin)) 202 | (end (plist-get args :end))) 203 | (unless file 204 | (user-error "Nothing to blame here")) 205 | (let-alist (magithub-repo) 206 | (let* ((default-directory (file-name-directory file)) 207 | (file (string-remove-prefix (magit-toplevel) file)) 208 | (git-rev (if use-default-branch 209 | .default_branch 210 | (magit-git-string "rev-parse" "HEAD"))) 211 | (anchor (magithub-browse-file--get-anchor begin end))) 212 | (unless (magithub-github-repository-p) 213 | (user-error "Not a GitHub repository")) 214 | (browse-url 215 | (format "%s/blame/%s/%s%s" .html_url git-rev file (or anchor ""))))))) 216 | 217 | (defvar magithub-after-create-messages 218 | '("Don't be shy!" 219 | "Don't let your dreams be dreams!") 220 | "One of these messages will be displayed after you create a 221 | GitHub repository.") 222 | 223 | (defun magithub-create (repo &optional org) 224 | "Create REPO on GitHub. 225 | 226 | If ORG is non-nil, it is an organization object under which to 227 | create the new repository. You must be a member of this 228 | organization." 229 | (interactive (if (or (not (magit-toplevel)) (magithub-github-repository-p)) 230 | (list nil nil) 231 | (let* ((ghub-username (ghubp-username)) ;performance 232 | (account (magithub--read-user-or-org)) 233 | (priv (magithub-confirm-no-error 'create-repo-as-private)) 234 | (reponame (magithub--read-repo-name account)) 235 | (desc (read-string "Description (optional): "))) 236 | (list 237 | `((name . ,reponame) 238 | (private . ,priv) 239 | (description . ,desc)) 240 | (unless (string= ghub-username account) 241 | `((login . ,account))))))) 242 | (when (magithub-github-repository-p) 243 | (error "Already in a GitHub repository")) 244 | (if (not (magit-toplevel)) 245 | (when (magithub-confirm-no-error 'init-repo-after-create) 246 | (magit-init default-directory) 247 | (call-interactively #'magithub-create)) 248 | (with-temp-message "Creating repository on GitHub..." 249 | (setq repo 250 | (magithub-request 251 | (if org 252 | (ghubp-post-orgs-org-repos org repo) 253 | (ghubp-post-user-repos repo))))) 254 | (magithub--random-message "Creating repository on GitHub...done!") 255 | (magit-status-internal default-directory) 256 | (magit-remote-add "origin" (magithub-repo--clone-url repo)) 257 | (magit-refresh) 258 | (when (magit-rev-verify "HEAD") 259 | (magit-push)))) 260 | 261 | (defun magithub--read-user-or-org () 262 | "Prompt for an account with completion. 263 | 264 | Candidates will include the current user and all organizations, 265 | public and private, of which they're a part. If there is only 266 | one candidate (i.e., no organizations), the single candidate will 267 | be returned without prompting the user." 268 | (let ((user (ghubp-username)) 269 | (orgs (ghubp-get-in-all '(login) 270 | (magithub-orgs-list))) 271 | candidates) 272 | (setq candidates orgs) 273 | (when user (push user candidates)) 274 | (cl-case (length candidates) 275 | (0 (user-error "No accounts found")) 276 | (1 (car candidates)) 277 | (t (completing-read "Account: " candidates nil t))))) 278 | 279 | (defun magithub--read-repo-name (for-user) 280 | (let* ((prompt (format "Repository name: %s/" for-user)) 281 | (dirnam (file-name-nondirectory (substring default-directory 0 -1))) 282 | (valid-regexp (rx bos (+ (any alnum "." "-" "_")) eos)) 283 | ret) 284 | ;; This is not very clever, but it gets the job done. I'd like to 285 | ;; either have instant feedback on what's valid or not allow users 286 | ;; to enter invalid names at all. Could code from Ivy be used? 287 | (while (not (s-matches-p valid-regexp 288 | (setq ret (read-string prompt nil nil dirnam)))) 289 | (message "invalid name") 290 | (sit-for 1)) 291 | ret)) 292 | 293 | (defun magithub--random-message (&optional prefix) 294 | (let ((msg (nth (random (length magithub-after-create-messages)) 295 | magithub-after-create-messages))) 296 | (if prefix (format "%s %s" prefix msg) msg))) 297 | 298 | (defun magithub-fork () 299 | "Fork 'origin' on GitHub." 300 | (interactive) 301 | (unless (magithub-github-repository-p) 302 | (user-error "Not a GitHub repository")) 303 | (magithub-confirm 'fork) 304 | (let* ((repo (magithub-repo)) 305 | (fork (with-temp-message "Forking repository on GitHub..." 306 | (magithub-request 307 | (ghubp-post-repos-owner-repo-forks repo))))) 308 | (when (magithub-confirm-no-error 'fork-create-spinoff) 309 | (call-interactively #'magit-branch-spinoff)) 310 | (magithub--random-message 311 | (let-alist repo (format "%s/%s forked!" .owner.login .name))) 312 | (let-alist fork 313 | (when (magithub-confirm-no-error 'fork-add-me-as-remote .owner.login) 314 | (magit-remote-add .owner.login (magithub-repo--clone-url fork)) 315 | (magit-set .owner.login "branch" (magit-get-current-branch) "pushRemote"))) 316 | (let-alist repo 317 | (when (magithub-confirm-no-error 'fork-set-upstream-to-me .owner.login) 318 | (call-interactively #'magit-branch..merge/remote))))) 319 | 320 | (defvar magithub-clone-history nil 321 | "History for `magithub-clone' prompt.") 322 | 323 | (defun magithub-clone--get-repo () 324 | "Prompt for a user and a repository. 325 | Returns a sparse repository object." 326 | (let ((user (ghubp-username)) 327 | (repo-regexp (rx bos (group (+ (not (any " ")))) 328 | "/" (group (+ (not (any " ")))) eos)) 329 | repo) 330 | (while (not (and repo (string-match repo-regexp repo))) 331 | (setq repo (read-from-minibuffer 332 | (concat 333 | "Clone GitHub repository " 334 | (if repo "(format is \"user/repo\"; C-g to quit)" "(user/repo)") 335 | ": ") 336 | (when user (concat user "/")) 337 | nil nil 'magithub-clone-history))) 338 | `((owner (login . ,(match-string 1 repo))) 339 | (name . ,(match-string 2 repo))))) 340 | 341 | (defcustom magithub-clone-default-directory nil 342 | "Default directory to clone to when using `magithub-clone'. 343 | When nil, the current directory at invocation is used." 344 | :type 'directory 345 | :group 'magithub) 346 | 347 | (defun magithub-clone (repo dir) 348 | "Clone REPO. 349 | Banned inside existing GitHub repositories if 350 | `magithub-clone-default-directory' is nil. 351 | 352 | See also `magithub-preferred-remote-method'." 353 | (interactive (let* ((repo (magithub-clone--get-repo)) 354 | (repo (or (magithub-request 355 | (ghubp-get-repos-owner-repo repo)) 356 | (let-alist repo 357 | (user-error "Repository %s/%s does not exist" 358 | .owner.login .name)))) 359 | (name (alist-get 'name repo)) 360 | (dirname (read-directory-name 361 | "Destination: " 362 | magithub-clone-default-directory 363 | name nil name))) 364 | (list repo dirname))) 365 | ;; Argument validation 366 | (unless (called-interactively-p 'any) 367 | (unless (setq repo (magithub-request 368 | (ghubp-get-repos-owner-repo repo))) 369 | (let-alist repo 370 | (user-error "Repository %s/%s does not exist" 371 | .owner.login .name)))) 372 | (let ((parent (file-name-directory dir))) 373 | (unless (file-exists-p parent) 374 | (when (magithub-confirm 'clone-create-directory parent) 375 | (mkdir parent t)))) 376 | (unless (file-writable-p dir) 377 | (user-error "%s is not writable" dir)) 378 | 379 | (let-alist repo 380 | (when (magithub-confirm-no-error 'clone .full_name dir) 381 | (let (set-upstream set-proxy) 382 | (setq set-upstream 383 | (and .fork (magithub-confirm-no-error 384 | 'clone-fork-set-upstream-to-parent 385 | .parent.full_name)) 386 | set-proxy 387 | (and set-upstream (magithub-confirm-no-error 388 | 'clone-fork-set-proxy-to-upstream))) 389 | (condition-case _ 390 | (let ((default-directory dir) 391 | (magit-clone-set-remote.pushDefault t)) 392 | (mkdir dir t) 393 | (magit-clone (magithub-repo--clone-url repo) dir) 394 | (add-function 395 | :after 396 | (process-sentinel magit-this-process) 397 | (lambda (process _event) 398 | (unless (process-live-p process) 399 | (when set-upstream 400 | (let ((upstream "upstream")) 401 | (when set-proxy (magit-set upstream "magithub.proxy")) 402 | (magit-remote-add upstream (magithub-repo--clone-url .parent)) 403 | (magit-branch..merge/remote (magit-get-current-branch) 404 | upstream)))))))))))) 405 | 406 | (defun magithub-clone--finished (user repo dir) 407 | "After finishing the clone, allow the user to jump to their new repo." 408 | (when (magithub-confirm-no-error 'clone-open-magit-status user repo dir) 409 | (magit-status-internal (s-chop-suffix "/" dir)))) 410 | 411 | (defun magithub-visit-thing () 412 | (interactive) 413 | (user-error 414 | (with-temp-buffer 415 | (use-local-map magithub-map) 416 | (substitute-command-keys 417 | "Deprecated; use `\\[magithub-browse-thing]' instead")))) 418 | 419 | (provide 'magithub) 420 | ;;; magithub.el ends here 421 | -------------------------------------------------------------------------------- /magithub.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Magithub -- Magit interfaces for GitHub 2 | #+AUTHOR: Sean Allred 3 | #+EMAIL: code@seanallred.com 4 | #+DATE: 2017-2018 5 | #+LANGUAGE: en 6 | 7 | #+TEXINFO_DIR_CATEGORY: Emacs 8 | #+TEXINFO_DIR_TITLE: Magithub: (magithub). 9 | #+TEXINFO_DIR_DESC: Magit interfaces for GitHub 10 | #+SUBTITLE: for version 0.1.5 (0.1.5-106-ge4a004c+1) 11 | #+BIND: ox-texinfo+-before-export-hook ox-texinfo+-update-version-strings 12 | 13 | #+TEXINFO_DEFFN: t 14 | #+OPTIONS: H:4 num:4 toc:2 15 | 16 | You may also be interested in [[https://github.com/vermiculus/magithub/tree/master/RelNotes][the most current release notes]]. 17 | 18 | Magithub provides an integrated GitHub experience through Magit's familiar 19 | interface. Just as Magit hopes to 'outsmart git', Magithub hopes to add 20 | smarts to GitHub for performing common tasks. 21 | 22 | Happy hacking! 23 | 24 | #+TEXINFO: @noindent 25 | This manual is for Magithub version 0.1.5 (0.1.5-106-ge4a004c+1). 26 | 27 | #+BEGIN_QUOTE 28 | Copyright (C) 2017-2018 Sean Allred 29 | 30 | You can redistribute this document and/or modify it under the terms 31 | of the GNU General Public License as published by the Free Software 32 | Foundation, either version 3 of the License, or (at your option) any 33 | later version. 34 | 35 | This document is distributed in the hope that it will be useful, 36 | but WITHOUT ANY WARRANTY; without even the implied warranty of 37 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 38 | General Public License for more details. 39 | #+END_QUOTE 40 | 41 | * Installation 42 | ** _ :ignore: 43 | 44 | Magithub can be installed from [[http://melpa.milkbox.net/#/magithub][MELPA]] using =M-x list-packages= or by 45 | evaluating the following: 46 | 47 | #+BEGIN_SRC elisp 48 | (package-install 'magithub) 49 | #+END_SRC 50 | 51 | Here is the basic recommended [[https://github.com/jwiegley/use-package][=use-package=]] configuration: 52 | 53 | #+BEGIN_SRC elisp 54 | (use-package magithub 55 | :after magit 56 | :ensure t 57 | :config (magithub-feature-autoinject t)) 58 | #+END_SRC 59 | 60 | If you prefer to install the package manually, this can of course be done 61 | via the usual means. 62 | 63 | For more information, see [[info:emacs#Packages]]. 64 | 65 | ** Authentication 66 | 67 | Given GitHub's rate-limiting policy, Magithub is unlikely to ever support 68 | running without authenticating. As such, you /must/ authenticate before you 69 | use Magithub. (As of #107, Magithub will not even attempt go online until 70 | you're properly authenticated.) 71 | 72 | To authenticate, you can simply start using Magithub; Ghub should walk you 73 | through the authentication process unless you use two-factor authentication. 74 | (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 75 | details.) 76 | 77 | If you do use two-factor authentication, you must 78 | 79 | 1. Manually create a GitHub token (from https://github.com/settings/tokens) 80 | for scopes `repo`, `notifications` and `user` (see variable 81 | ~magithub-github-token-scopes~) 82 | 2. Store it for Magithub per user in one of your ~auth-sources~ 83 | (e.g. =~/.authinfo=). Write a line like this: 84 | 85 | #+BEGIN_EXAMPLE 86 | machine api.github.com login YOUR_GITHUB_USERNAME^magithub password YOUR_GITHUB_TOKEN 87 | #+END_EXAMPLE 88 | 89 | Beware that writing the token in plaintext in =~/.authinfo= (or elsewhere) is 90 | not secure against attackers with access to that file. For details and 91 | 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 92 | 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]]. 93 | 94 | If you want to authenticate Ghub without using Magithub, you can simply 95 | evaluate the following: 96 | 97 | #+BEGIN_SRC emacs-lisp 98 | (require 'magithub) 99 | (ghub-get "/user" nil :auth 'magithub) 100 | #+END_SRC 101 | 102 | After Ghub walks you through the authentication process during evaluation, 103 | the ~ghub-get~ form should return familiar information (your login, email, 104 | etc.). 105 | 106 | If you're having trouble /authenticating/, [[https://github.com/magit/ghub/issues/new][open a Ghub issue]] or drop by 107 | [[https://gitter.im/vermiculus/magithub][Magithub's]] or [[https://gitter.im/magit/magit][Magit's]] Gitter channel. 108 | 109 | ** Enterprise Support 110 | 111 | For GitHub Enterprise support, you'll need to add your enterprise domain to 112 | ~magithub-github-hosts~ so that Magithub can detect when it's in a GitHub 113 | repository. 114 | 115 | #+BEGIN_SRC elisp 116 | (use-package magithub 117 | :ensure t 118 | :config 119 | (magithub-feature-autoinject t) 120 | (setq magithub-github-hosts '("github.enterprise.domain/api/v3"))) 121 | #+END_SRC 122 | 123 | Next, you will need to create a personal access token and add it to 124 | your =~/.authinfo= file to authenticate to your domain; see Ghub's 125 | manual for details. 126 | 127 | #+BEGIN_EXAMPLE 128 | machine github.enterprise.domain/api/v3 login YOUR_USERNAME^magithub password YOUR_GITHUB_TOKEN 129 | #+END_EXAMPLE 130 | 131 | Finally, for each github repository you with to use with GHE, you will 132 | need to add a ~github.host~ config and a ~DOMAIN.user~ config. 133 | 134 | #+BEGIN_SRC shell 135 | # where GHE_URL is the url to your v3 api endpoint, and USERNAME is your GHE username 136 | GHE_URL=github.enterprise.domain/api/v3; git config github.host ${GHE_URL}; git config ${GHE_URL}.user $USERNAME 137 | #+END_SRC 138 | 139 | * Introduction 140 | ** _ :ignore: 141 | 142 | Magithub tries to follow closely Magit's lead in general interface. Most of 143 | its functionality is developed to tightly integrate with its section/ 144 | framework. See [[https://magit.vc/manual/magit/Sections.html#Sections][Magit's documentation]] for information on how to navigate 145 | using this framework. 146 | 147 | Magithub's functionality uses section-specific keymaps to expose 148 | functionality. Where it makes sense, the following keys will map to 149 | functions that 'do the right thing': 150 | 151 | - Key: w, magithub-browse-thing 152 | 153 | Open a browser to the thing at point. For instance, when point is on 154 | issue 42 in your-favorite/github-repo, we'll open 155 | =http://github.com/your-favorite/github-repo/issue/42=. 156 | 157 | - Key: a, magithub-add-thing 158 | 159 | Add something to the thing at point. For instance, on a list of labels, 160 | you can add more labels. 161 | 162 | - Key: e, magithub-edit-thing 163 | 164 | Edit the thing at point, such as an issue. 165 | 166 | - Key: r, magithub-reply-thing 167 | 168 | Reply to the thing at point, such as a comment. 169 | 170 | Magithub also considers the similar placeholder commands introduced by Magit 171 | which you may already be familiar with: 172 | 173 | - Key: k, magit-delete-thing 174 | - Key: RET, magit-visit-thing 175 | 176 | These concepts are intended to provide a more consistent experience 177 | throughout Magithub within Magit by categorizing your broader interactions 178 | with all GitHub content. As with Magit, more commands are added as the 179 | situation calls for it. 180 | 181 | ** Note 182 | 183 | By default, Magithub enables itself in all repositories where =origin= points 184 | to GitHub. 185 | 186 | - User Option: magithub-enabled-by-default 187 | 188 | When non-nil, Magithub is enabled by default. This is the fallback value 189 | of git variable =magithub.enabled= is not set in this repository. 190 | 191 | - User Option: magithub-github-hosts 192 | 193 | A list of top-level domains that should be recognized as GitHub hosts. 194 | 195 | ** Brief Tutorial 196 | 197 | Here's a script that will guide you through the major features of Magithub. 198 | This is not a replacement for the documentation, but rather an example 199 | workflow to whet your appetite. 200 | 201 | *** Clone a repository 202 | #+BEGIN_EXAMPLE 203 | M-x magithub-clone RET vermiculus/my-new-repository 204 | #+END_EXAMPLE 205 | Cloning a repository this way gets the clone URL from GitHub and forwards 206 | that on to ~magit-clone~. If the repository is a fork, you're prompted to add 207 | the parent is added under the =upstream= remote. 208 | 209 | Fork behavior may change in the future. It may be more appropriate to 210 | actually/ clone the source repository and add your remote as a fork. This 211 | will cover the 90% case (the 10% case being active forks of unmaintained 212 | projects). 213 | 214 | *** Viewing project status 215 | You are dropped into a status buffer for =vermiculus/my-new-repository=. You 216 | see some open issues and pull requests. You move your cursor to an issue of 217 | interest and =TAB= to expand it, seeing the author, when it was 218 | created/updated, any labels, and a preview of the issue contents. 219 | 220 | If =vermiculus/my-new-repository= used any status checks, you would see those 221 | statuses as a header in this buffer. 222 | 223 | *** Viewing and replying to an issue 224 | You =RET= on the issue and are taken to a dedicated buffer for that issue. 225 | You can now see its full contents as well as all comments. You'd like to 226 | leave a comment -- a suggestion for a fix or an additional use-case to 227 | consider -- you press =r= to open a new buffer to /reply/ to this issue. You 228 | write your comment and =C-c C-c= to submit. But, oh no! You didn't turn on 229 | =flyspell-mode= in markdown buffers, so you submitted a spelling error. A 230 | simple =e= on the comment will /edit/ it. After submitting again with =C-c C-c=, 231 | everything is well. 232 | 233 | Right now, other activity on the issue is not inserted into this buffer. 234 | Press =w= to open the issue in your browser. 235 | 236 | *** Creating an issue 237 | You notice a small issue in how some feature is implemented, so back in the 238 | status buffer, you use =H i= to create a new issue. (While inside the GitHub 239 | repository, you could've used any key bound to ~magithub-issue-new~.) The 240 | first line is the title of the new issue; everything else is the body. You 241 | submit the issue with =C-c C-c=. 242 | 243 | From here you will be prompted to add labels by selecting them from the list 244 | and adding them with =RET=. To skip adding labels and submit the issue without 245 | any you can enter "" in the field and then =RET=. 246 | 247 | /Note: your completion framework may have special functionality to enter null 248 | here (ie. in Ivy you must use =C-M-j= to accept without input)./ 249 | 250 | You come back a little while later to leave additional details -- you reply 251 | to your own issue in a comment, but realize you should just edit your 252 | original issue to avoid confusion. You =k= to /kill/ / delete the comment. 253 | 254 | *** Creating a pull request 255 | Since you care about this project and want to help it succeed, you decide to 256 | fix this issue yourself. You checkout a new branch (=b c my-feature RET=) and 257 | get to work. 258 | 259 | Because you're so /awesome/, you're ready to push your commit to fix your 260 | issue. After realizing you don't have push permissions to this repository, 261 | you create a fork using =H f=. You push your branch to your new remote (named 262 | after your username) and create a pull request with =H p=. You select the 263 | head branch as =my-feature= and the base branch as =master= (or whatever the 264 | production/staging branch is for the project). You fill out the pull 265 | request template provided by the project (and inserted into your PR) and off 266 | you go! 267 | 268 | * Status Buffer Integration 269 | 270 | The part of Magithub you're likely to interact with the most is 271 | embedded right into Magit's status buffer. 272 | 273 | - Key: H, magithub-dispatch-popup 274 | 275 | Access many Magithub entry-points. See [[*Dispatch Popup]] for more details. 276 | 277 | - Key: H C e, FIXME 278 | 279 | Toggle status buffer integration in this repository. 280 | 281 | There are two integrations turned on by default: 282 | 283 | ** Project Status 284 | 285 | Many services (such as Travis CI and CircleCI) will post statuses to 286 | commits. A summary of these statuses are visible in the status buffer 287 | headers. Note that the branch must have a [[https://magit.vc/manual/magit/The-Two-Remotes.html#The-Two-Remotes][push-remote]] set in order to 288 | find the correct status to use. 289 | 290 | - Key: RET, magithub-ci-visit 291 | - Key: w, magithub-ci-visit 292 | 293 | Visit the service's summary of this status. For example, a status posted 294 | by Travis CI will open that build on Travis. 295 | 296 | - Key: g, magithub-ci-refresh 297 | 298 | Refresh statuses from GitHub and then refresh the current buffer. 299 | 300 | - Key: H C s, FIXME 301 | 302 | Enable/disable status checks in this repository. 303 | 304 | ** Open Issues and Pull Requests 305 | 306 | These will also display in the status buffer. There's a lot of 307 | functionality available right from an issue section. 308 | 309 | - Key: g, magithub-issue-refresh 310 | 311 | Refresh issues and pull requests from GitHub and then refresh the current 312 | buffer. 313 | 314 | - Key: RET, magithub-issue-visit 315 | 316 | Open a new buffer to view an issue and its comments. 317 | 318 | - Key: w, magithub-issue-browse 319 | - Key: w, magithub-pull-browse 320 | 321 | Browse this issue / pull request on GitHub. 322 | 323 | - Key: O, magithub-issue-open 324 | - Key: C, magithub-issue-close 325 | 326 | Open/close an issue. 327 | 328 | - Key: N, magithub-issue-personal-note 329 | 330 | Opens a buffer for offline note-taking. 331 | 332 | - Key: L, magithub-issue-add-labels 333 | 334 | Add labels to the issue. 335 | 336 | - Key: a, magithub-label-add 337 | - Key: k, magithub-label-remove 338 | 339 | When point is on a label section, you can add/remove labels (provided you 340 | have permission to do so). 341 | 342 | - Command: magithub-label-color-replace 343 | 344 | Labels are colored as they would be on GitHub. In some themes, this 345 | produces an illegible or otherwise undesirable color. This command can 346 | help you find a substitute for labels of this color. 347 | 348 | - Variable: magithub-issue-details-hook 349 | 350 | Control which issue details display in the status buffer. Functions 351 | intended for this variable use the =magithub-issue-detail-insert-*= prefix. 352 | 353 | Performance note: judicious use of this variable can improve your overall 354 | Magit experience in large buffers. 355 | 356 | - User Option: magithub-issue-issue-filter-functions 357 | - User Option: magithub-issue-pull-request-filter-functions 358 | 359 | These are lists of functions which must all return non-nil for an issue/PR 360 | to be displayed in the status buffer. They all receive the issue/PR 361 | object as their sole argument. For example, you might want to filter out 362 | issues labels =enhancement= from your list: 363 | 364 | #+BEGIN_SRC emacs-lisp 365 | (setq magithub-issue-issue-filter-functions 366 | (list (lambda (issue) ; don't show enhancement requests 367 | (not 368 | (member "enhancement" 369 | (let-alist issue 370 | (ghubp-get-in-all '(name) .labels))))))) 371 | #+END_SRC 372 | 373 | *** Manipulating the Cache 374 | When point is on a Magithub-controlled section (like the status header): 375 | | Default Key | Description | 376 | |-------------+--------------------------------------------| 377 | | =g= | Refresh only this section's GitHub content | 378 | | =C-u g= | Like =g=, but works on the whole buffer | 379 | 380 | *** Offline Mode 381 | | Default Key | Description | 382 | |-------------+---------------------| 383 | | =H C c= | Toggle offline mode | 384 | 385 | Offline mode was introduced for those times when you're on the go, but you'd 386 | still like to have an overview of GitHub data in your status buffer. It's 387 | also useful for folks who want to explicitly control when Emacs communicates 388 | with GitHub -- for this purpose, you can use =C-u g= (discussed above) to pull 389 | data from GitHub while in offline mode. 390 | 391 | To start into offline mode everywhere, use 392 | #+BEGIN_SRC sh 393 | git config --global magithub.cache always 394 | #+END_SRC 395 | 396 | See the documentation for function ~magithub-settings--set-magithub.cache~ 397 | for details on appropriate values. 398 | 399 | *** Controlling Sections 400 | 401 | Sections like the issue list and the status header can be toggled with the 402 | interactive functions of the form =magithub-toggle-*=. These functions have 403 | no default keybinding. 404 | 405 | Since status checks can be API-hungry and not all projects use them, you can 406 | disable the status header at the repository-level with =H ~=; see the Status 407 | Checks section for more information. 408 | 409 | * Dispatch Popup 410 | 411 | Much of Magithub's functionality, including configuration options, is behind 412 | this popup. In Magit status buffers, it's bound to =H=. 413 | 414 | - Key: d, magithub-dashboard 415 | 416 | See [[*Dashboard]]. 417 | 418 | - Key: c, magithub-create 419 | 420 | Push a local repository up to GitHub. 421 | 422 | - Key: H, magithub-browse 423 | 424 | Open the current repository in your browser. 425 | 426 | - Key: f, magithub-fork 427 | 428 | Fork this repository on GitHub. This will add your fork as a remote under 429 | your username. For example, if user =octocat= forked Magit, we would see a 430 | new remote called =octocat= pointing to =octocat/magit=. 431 | 432 | - Key: i, magithub-issue-new 433 | - Key: p, magithub-pull-request-new 434 | 435 | Open a new buffer to create an issue or open a pull request. See 436 | [[*Creating Content]]. 437 | 438 | ** Configuration 439 | 440 | Per-repository configuration is controlled via git variables reachable from 441 | the dispatch popup via =H C=. Use =? = to get online help for each 442 | variable in that popup. 443 | 444 | - Key: C e, FIXME 445 | 446 | Turn Magithub on/off (completely). 447 | 448 | - Key: C s, FIXME 449 | 450 | Turn the project status header on/off. 451 | 452 | - Key: C c, FIXME 453 | 454 | Control whether Magithub is considered 'online'. This controls the 455 | behavior of the the cache. This may go away in the future. See 456 | [[*Manipulating the Cache]] for more details. 457 | 458 | - Key: C i, FIXME 459 | 460 | Toggle the issues section. 461 | 462 | - Key: C p, FIXME 463 | 464 | Toggle the pull requests section. 465 | 466 | - Key: C x, FIXME 467 | 468 | Set the 'proxy' used for this repository. See [[*Proxies]]. 469 | 470 | ** Meta 471 | 472 | Since Magithub is so integrated with Magit, there's often confusion about 473 | whom to ask for support (especially for users of preconfigured Emacsen like 474 | Spacemacs and Prelude). Hopefully, these functions can direct you to the 475 | appropriate spot. 476 | 477 | - Key: &, magithub--meta-new-issue 478 | 479 | Open the browser to create a new issue for Magithub functionality 480 | described in this document. 481 | 482 | - Key: h, magithub--meta-help 483 | 484 | Open the browser to ask for help on Gitter, a GitHub-focused chatroom. 485 | 486 | * 'Features' 487 | 488 | Given that some features of Magithub are not desired by or appropriate for 489 | every type of user, there are features that are not turned on by default. 490 | These are features that are injected into standard Magit popups. 491 | 492 | The list of available features is available in constant 493 | ~magithub-feature-list~. Despite its name, this is an alist of symbols (i.e., 494 | 'features') to functions that install the feature. While the documentation 495 | for each feature lives in that symbol, you would normally not otherwise 496 | interact with it. 497 | 498 | - Function: magithub-feature-autoinject 499 | 500 | This function is the expected interface to install features. You will 501 | normally use 502 | #+BEGIN_SRC emacs-lisp 503 | (magithub-feature-autoinject t) 504 | #+END_SRC 505 | in your configuration to install all features, but you have the option of 506 | installing them one at a time using the symbols from constant 507 | ~magithub-feature-list~ or as a list of those symbols: 508 | #+BEGIN_SRC emacs-lisp 509 | (magithub-feature-autoinject 'commit-browse) 510 | (magithub-feature-autoinject '(commit-browse pull-request-merge)) 511 | #+END_SRC 512 | 513 | * Cloning 514 | 515 | - Command: magithub-clone 516 | 517 | Clone a repository from GitHub. 518 | 519 | - User Option: magithub-clone-default-directory 520 | 521 | The default destination directory to use for cloning. 522 | 523 | - User Option: magithub-preferred-remote-method 524 | 525 | This option is a symbol indicating the preferred cloning method (between 526 | HTTPS, SSH, and the =git://= protocol). 527 | 528 | * Dashboard 529 | 530 | The dashboard shows you information pertaining to /you/: 531 | - notifications 532 | - issues and pull requests you're assigned per repository 533 | as well as contextual information like the logged-in user and [[https://developer.github.com/v3/#rate-limiting][rate-limiting]] 534 | information. 535 | 536 | - Command: magithub-dashboard 537 | 538 | View your dashboard. 539 | 540 | - Key: ;, magithub-dashboard-popup 541 | 542 | Configure your global dashboard settings. 543 | 544 | - User Option: magithub-dashboard-show-read-notifications 545 | 546 | When non-nil, we'll show read notifications in the dashboard. 547 | 548 | * Creating Content 549 | 550 | It's great to read about what's been happening, but it's even better to 551 | contribute your own thoughts and activity! 552 | 553 | - Key: H i, magithub-issue-new 554 | - Key: H p, magithub-pull-request-new 555 | 556 | Create issues and pull requests. If you have push access to the 557 | repository, you'll have the opportunity to add labels before you submit 558 | the issue. 559 | 560 | Creating a pull request requires a HEAD branch, a BASE branch, and to know 561 | which remote points to your fork. 562 | 563 | - Key: r, magithub-comment-new 564 | - Key: r, magithub-comment-reply 565 | 566 | On an issue or pull request section, ~magithub-comment-new~ will allow you 567 | to post a comment to that issue/PR. If point is already on a comment, 568 | ~magithub-comment-reply~ will quote the comment at point for you. 569 | 570 | * Caching 571 | 572 | Caching is a complicated topic with a long Magithub history of, well, 573 | failure. As of today, all data retrieved from the API is cached by 574 | default. Using =g= on Magithub sections will usually refresh the information 575 | in the buffer pertaining to that section. Otherwise, =C-u g= in any Magit 576 | buffer will refresh all GitHub data in that buffer. 577 | 578 | This behavior may change in the future, but for now, it's the most stable 579 | option. See 580 | 581 | * Proxies 582 | 583 | It's not uncommon to have repositories where the bug-tracker is in a 584 | separate repository. For these cases, you can use the idea of 'proxies'. A 585 | proxy is a remote (with a GitHub-associated URL) that you choose to use for 586 | all GitHub API requests concerning the /actual/ current repository. This is 587 | manifest in the git variable =magithub.proxy=. 588 | 589 | - Key: H C x, magithub-settings--set-magithub.contextRemote 590 | 591 | If you consistently use a specific remote name for the bug tracker, you 592 | can set it globally. 593 | 594 | All GitHub requests specific to the current repository context are routed 595 | through ~magithub-repo~ which respects this proxy. 596 | 597 | * Configuring 598 | 599 | Magithub uses a standardized configuration scheme implemented using Git 600 | variables. This allows your Magithub configuration to use all the powerful 601 | features of =git-config(1)= and allows tight integration into Magit's existing 602 | repository configuration workflows. 603 | 604 | To get the most up-to-date list of configuration options, use 605 | #+BEGIN_SRC example 606 | M-x apropos-command RET magithub-settings--set 607 | #+END_SRC 608 | to summarize them all. If an important option is missing from this manual, 609 | reports and pull requests are welcome! 610 | 611 | The decision to implement these as Git variables stems from the varying size 612 | of project repositories: it is extremely common to contribute to 613 | exceptionally large repositories where including, say, the 'issues' section 614 | would bring Emacs to its knees -- but it is equally common to work on 615 | smaller repositories where such concern is negligible and the issues section 616 | is a nice feature. 617 | 618 | * Unfiled 619 | ** Content 620 | *** Working with Repositories 621 | **** DONE General 622 | | Default Key | Description | 623 | |--------------------+------------------------------------------------| 624 | | =H H= | Opens the current repository in the browser | 625 | | =H c= | Creates the current local repository on GitHub | 626 | | =M-x magithub-clone= | Clone a repository | 627 | 628 | =magithub-clone= may appear to be a thin wrapper over =magit-clone=, but it's 629 | quite a bit smarter than that. We'll of course respect 630 | =magithub-preferred-remote-method= when cloning the repository, but we can 631 | also detect when the repository is a fork and can create and set an upstream 632 | remote accordingly (similar to =M-x magithub-fork=). 633 | 634 | **** DONE Issues 635 | | Default Key | Description | 636 | |-------------+--------------------------| 637 | | =H i= | Create a new issue | 638 | | =RET= | Open the issue in GitHub | 639 | 640 | You can filter issues with =magithub-issue-issue-filter-functions=: 641 | #+BEGIN_SRC emacs-lisp 642 | (setq magithub-issue-issue-filter-functions 643 | (list (lambda (issue) ; don't show enhancement requests 644 | (not 645 | (member "enhancement" 646 | (let-alist issue 647 | (ghubp-get-in-all '(name) .labels))))))) 648 | #+END_SRC 649 | Each function in the =*-functions= list must return non-nil for the issue to 650 | appear in the issue list. See also the documentation for that variable. 651 | 652 | **** DONE Forking and Pull Requests 653 | | Default Key | Description | 654 | |-------------+-------------------------------| 655 | | =H f= | Fork the current repository | 656 | | =H p= | Submit pull requests upstream | 657 | 658 | You can also filter pull requests with 659 | =magithub-issue-pull-request-filter-functions=. See the section on 660 | issue-filtering for an example. 661 | 662 | **** TODO Labels 663 | | Default Key | Description | 664 | |----------------------------------+-------------------------------------------| 665 | | =M-x magithub-label-color-replace= | Choose a new color for the label at point | 666 | 667 | By default, Magithub will adopt the color used by GitHub when showing 668 | labels. In some themes, this doesn't provide enough contrast. Use =M-x 669 | magithub-label-color-replace= to replace the current label's color with 670 | another one. (This will apply to all labels in all repositories, but will 671 | of course not apply to all /shades/ of the original color.) 672 | 673 | **** TODO Status Checks 674 | | Default Key | Description | 675 | |-------------+--------------------------------------------------| 676 | | =RET= | Visit the status's dashboard in your browser | 677 | | =TAB= | On the status header, show individual CI details | 678 | | =H ~= | Toggle status integration for this repository | 679 | 680 | When the status buffer first opens, the status header is inserted at the top 681 | and probably looks something like this: 682 | #+BEGIN_EXAMPLE 683 | Status: Success 684 | #+END_EXAMPLE 685 | 686 | You can get a breakdown of which checks succeeded and which failed by using 687 | =TAB=: 688 | #+BEGIN_EXAMPLE 689 | Status: Success 690 | Checks for ref: develop 691 | Success The Travis CI build passed continuous-integration/travis-ci/push 692 | #+END_EXAMPLE 693 | 694 | Pressing =RET= on the header will take you to the dashboard associated with 695 | that status check. If there's more than one status check here, you'll be 696 | prompted to choose a check (e.g., Travis, Circle, CLA, ...). Of course, if 697 | you expand the header to show the individual checks, =RET= on those will take 698 | you straight to that check. 699 | 700 | *** TODO Your Dashboard 701 | Check out =M-x magithub-dashboard= to view your notifications and issues 702 | assigned to you 703 | 704 | ** TODO 'Tricks' 705 | 706 | Most of Magithub is implemented in pure Elisp now, but there are a few 707 | lingering goodies that haven't been ported (since their real logic is 708 | non-trivial). These definitions are relegated to =magithub-issue-tricks.el=. 709 | 710 | Make sure to install [[https://hub.github.com][=hub=]] and add it to your ~exec-path~ if you intend to use 711 | these functions. After installation, use =hub browse= from a directory with a 712 | GitHub repository to force the program to authenticate -- this avoids some 713 | weirdness on the Emacs side of things. 714 | 715 | * _ Copying 716 | :PROPERTIES: 717 | :COPYING: t 718 | :END: 719 | 720 | #+BEGIN_QUOTE 721 | Copyright (C) 2017-2018 Sean Allred 722 | 723 | You can redistribute this document and/or modify it under the terms 724 | of the GNU General Public License as published by the Free Software 725 | Foundation, either version 3 of the License, or (at your option) any 726 | later version. 727 | 728 | This document is distributed in the hope that it will be useful, 729 | but WITHOUT ANY WARRANTY; without even the implied warranty of 730 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 731 | General Public License for more details. 732 | #+END_QUOTE 733 | 734 | * _ :ignore: 735 | 736 | # IMPORTANT: Also update ORG_ARGS and ORG_EVAL in the Makefile. 737 | # Local Variables: 738 | # fill-column: 76 739 | # eval: (require 'ox-extra nil t) 740 | # eval: (require 'ox-texinfo+ nil t) 741 | # eval: (and (featurep 'ox-extra) (ox-extras-activate '(ignore-headlines))) 742 | # indent-tabs-mode: nil 743 | # org-src-preserve-indentation: nil 744 | # End: 745 | -------------------------------------------------------------------------------- /screenshots.md: -------------------------------------------------------------------------------- 1 | # Screenshots 2 | 3 | This file indexes screenshots and captured workflows from various 4 | stages in development. Some features may have been added or changed 5 | than what appears here, although I hope to keep it up-to-date! 6 | 7 | ## Creating a Repository 8 | 9 | ![Creating](images/create.gif) 10 | 11 | ## Submitting a Pull Request 12 | 13 | ![submitting a Pull Request](images/pull-request.gif) 14 | 15 | ## CI Status 16 | 17 | ![CI Pending](images/ci-pending.png) 18 | 19 | ![CI Failure](images/ci-failure.png) 20 | 21 | ![CI Success](images/ci-success.png) 22 | 23 | --- 24 | 25 | ![Dispatch](images/scr1.png)|![Creating](images/scr2.png) 26 | :-------------------------:|:-------------------------: 27 | ![Forking](images/scr3.png)|![Pushing](images/scr4.png) 28 | ![Issues and Pull Requests](images/scr5.png)| 29 | -------------------------------------------------------------------------------- /test/magithub-test.el: -------------------------------------------------------------------------------- 1 | ;;; magithub-tests.el --- tests for Magithub 2 | 3 | ;; Copyright (C) 2016-2018 Sean Allred 4 | ;; 5 | ;; License: GPLv3 6 | 7 | ;;; Code: 8 | 9 | (require 'ert) 10 | (require 'magithub-core) 11 | (require 'ghub+) 12 | 13 | (setq ghubp-request-override-function 14 | #'magithub-mock-ghub-request) 15 | 16 | (defmacro magithub-test-cache-with-new-cache (plist &rest body) 17 | (declare (indent 1)) 18 | `(let ((magithub-cache-class-refresh-seconds-alist ',plist) 19 | (magithub-cache--cache (make-hash-table))) 20 | ,@body)) 21 | 22 | (ert-deftest magithub-test-cache () 23 | (magithub-test-cache-with-new-cache ((:test . 30)) 24 | (should (equal t (magithub-cache :test t))))) 25 | 26 | (ert-deftest magithub-test-origin-parse () 27 | "Tests issue #105." 28 | (let ((repo '((owner (login . "vermiculus")) 29 | (name . "magithub")))) 30 | (should (equal repo (magithub--url->repo "git@github.com:vermiculus/magithub.git"))) 31 | (should (equal repo (magithub--url->repo "git@github.com:vermiculus/magithub"))) 32 | (should (equal repo (magithub--url->repo "git+ssh://github.com/vermiculus/magithub"))) 33 | (should (equal repo (magithub--url->repo "ssh://git@github.com/vermiculus/magithub"))))) 34 | 35 | (ert-deftest magithub-test-source-repo () 36 | "Test basic API functionality. 37 | This tests everything from checking API availability to 38 | determining that we're in a GitHub repository to actually making 39 | cached API calls." 40 | (let ((magithub--api-last-checked (current-time))) 41 | (should (magithub-source--sparse-repo)) 42 | (should (magithub-repo)) 43 | (should (let ((magithub-cache--refresh t)) ; force API call 44 | (magithub-repo))) 45 | (should (magithub-repo)))) ; force cache read 46 | 47 | (ert-deftest magithub-test-parse-time-string () 48 | "Test parsing of datetime." 49 | (should (equal '(23253 12274) (magithub--parse-time-string "2018-04-16T23:21:22Z"))) 50 | (should (equal '(23253 12274) (magithub--parse-time-string "2018-04-16T23:21:22"))) 51 | (should (equal '(23253 12274) (magithub--parse-time-string "2018-04-16T2321:22"))) 52 | (should-error (magithub--parse-time-string "2018-04-16T23:21:2XZ"))) 53 | 54 | ;;; magithub-test.el ends here 55 | -------------------------------------------------------------------------------- /test/mock-data/get/repos.d/vermiculus.d/magithub.81a9dfc7: -------------------------------------------------------------------------------- 1 | ((id . 68352724) 2 | (name . "magithub") 3 | (full_name . "vermiculus/magithub") 4 | (owner 5 | (login . "vermiculus") 6 | (id . 2082195) 7 | (avatar_url . "https://avatars3.githubusercontent.com/u/2082195?v=4") 8 | (gravatar_id . "") 9 | (url . "https://api.github.com/users/vermiculus") 10 | (html_url . "https://github.com/vermiculus") 11 | (followers_url . "https://api.github.com/users/vermiculus/followers") 12 | (following_url . "https://api.github.com/users/vermiculus/following{/other_user}") 13 | (gists_url . "https://api.github.com/users/vermiculus/gists{/gist_id}") 14 | (starred_url . "https://api.github.com/users/vermiculus/starred{/owner}{/repo}") 15 | (subscriptions_url . "https://api.github.com/users/vermiculus/subscriptions") 16 | (organizations_url . "https://api.github.com/users/vermiculus/orgs") 17 | (repos_url . "https://api.github.com/users/vermiculus/repos") 18 | (events_url . "https://api.github.com/users/vermiculus/events{/privacy}") 19 | (received_events_url . "https://api.github.com/users/vermiculus/received_events") 20 | (type . "User") 21 | (site_admin)) 22 | (private) 23 | (html_url . "https://github.com/vermiculus/magithub") 24 | (description . "Magit interfaces for GitHub") 25 | (fork) 26 | (url . "https://api.github.com/repos/vermiculus/magithub") 27 | (forks_url . "https://api.github.com/repos/vermiculus/magithub/forks") 28 | (keys_url . "https://api.github.com/repos/vermiculus/magithub/keys{/key_id}") 29 | (collaborators_url . "https://api.github.com/repos/vermiculus/magithub/collaborators{/collaborator}") 30 | (teams_url . "https://api.github.com/repos/vermiculus/magithub/teams") 31 | (hooks_url . "https://api.github.com/repos/vermiculus/magithub/hooks") 32 | (issue_events_url . "https://api.github.com/repos/vermiculus/magithub/issues/events{/number}") 33 | (events_url . "https://api.github.com/repos/vermiculus/magithub/events") 34 | (assignees_url . "https://api.github.com/repos/vermiculus/magithub/assignees{/user}") 35 | (branches_url . "https://api.github.com/repos/vermiculus/magithub/branches{/branch}") 36 | (tags_url . "https://api.github.com/repos/vermiculus/magithub/tags") 37 | (blobs_url . "https://api.github.com/repos/vermiculus/magithub/git/blobs{/sha}") 38 | (git_tags_url . "https://api.github.com/repos/vermiculus/magithub/git/tags{/sha}") 39 | (git_refs_url . "https://api.github.com/repos/vermiculus/magithub/git/refs{/sha}") 40 | (trees_url . "https://api.github.com/repos/vermiculus/magithub/git/trees{/sha}") 41 | (statuses_url . "https://api.github.com/repos/vermiculus/magithub/statuses/{sha}") 42 | (languages_url . "https://api.github.com/repos/vermiculus/magithub/languages") 43 | (stargazers_url . "https://api.github.com/repos/vermiculus/magithub/stargazers") 44 | (contributors_url . "https://api.github.com/repos/vermiculus/magithub/contributors") 45 | (subscribers_url . "https://api.github.com/repos/vermiculus/magithub/subscribers") 46 | (subscription_url . "https://api.github.com/repos/vermiculus/magithub/subscription") 47 | (commits_url . "https://api.github.com/repos/vermiculus/magithub/commits{/sha}") 48 | (git_commits_url . "https://api.github.com/repos/vermiculus/magithub/git/commits{/sha}") 49 | (comments_url . "https://api.github.com/repos/vermiculus/magithub/comments{/number}") 50 | (issue_comment_url . "https://api.github.com/repos/vermiculus/magithub/issues/comments{/number}") 51 | (contents_url . "https://api.github.com/repos/vermiculus/magithub/contents/{+path}") 52 | (compare_url . "https://api.github.com/repos/vermiculus/magithub/compare/{base}...{head}") 53 | (merges_url . "https://api.github.com/repos/vermiculus/magithub/merges") 54 | (archive_url . "https://api.github.com/repos/vermiculus/magithub/{archive_format}{/ref}") 55 | (downloads_url . "https://api.github.com/repos/vermiculus/magithub/downloads") 56 | (issues_url . "https://api.github.com/repos/vermiculus/magithub/issues{/number}") 57 | (pulls_url . "https://api.github.com/repos/vermiculus/magithub/pulls{/number}") 58 | (milestones_url . "https://api.github.com/repos/vermiculus/magithub/milestones{/number}") 59 | (notifications_url . "https://api.github.com/repos/vermiculus/magithub/notifications{?since,all,participating}") 60 | (labels_url . "https://api.github.com/repos/vermiculus/magithub/labels{/name}") 61 | (releases_url . "https://api.github.com/repos/vermiculus/magithub/releases{/id}") 62 | (deployments_url . "https://api.github.com/repos/vermiculus/magithub/deployments") 63 | (created_at . "2016-09-16T04:32:34Z") 64 | (updated_at . "2017-09-29T11:26:51Z") 65 | (pushed_at . "2017-09-18T20:06:43Z") 66 | (git_url . "git://github.com/vermiculus/magithub.git") 67 | (ssh_url . "git@github.com:vermiculus/magithub.git") 68 | (clone_url . "https://github.com/vermiculus/magithub.git") 69 | (svn_url . "https://github.com/vermiculus/magithub") 70 | (homepage . "") 71 | (size . 1841) 72 | (stargazers_count . 290) 73 | (watchers_count . 290) 74 | (language . "Emacs Lisp") 75 | (has_issues . t) 76 | (has_projects . t) 77 | (has_downloads . t) 78 | (has_wiki . t) 79 | (has_pages) 80 | (forks_count . 27) 81 | (mirror_url) 82 | (open_issues_count . 35) 83 | (forks . 27) 84 | (open_issues . 35) 85 | (watchers . 290) 86 | (default_branch . "master") 87 | (permissions 88 | (admin . t) 89 | (push . t) 90 | (pull . t)) 91 | (allow_squash_merge . t) 92 | (allow_merge_commit . t) 93 | (allow_rebase_merge . t) 94 | (network_count . 27) 95 | (subscribers_count . 15)) 96 | -------------------------------------------------------------------------------- /test/test-helper.el: -------------------------------------------------------------------------------- 1 | ;;; Allow loading package files 2 | (require 'cl-lib) 3 | 4 | (defun magithub-in-test-dir (file) 5 | "Expand FILE in the test directory." 6 | (let ((dir default-directory)) 7 | (while (and (not (string= dir "/")) 8 | (not (file-exists-p (expand-file-name ".git" dir)))) 9 | (setq dir (file-name-directory (directory-file-name dir)))) 10 | (when (string= dir "/") 11 | (error "Project root not found")) 12 | (setq dir (expand-file-name "test" dir)) 13 | (expand-file-name file dir))) 14 | 15 | (defun magithub-mock-data-crunch (data) 16 | "Crunch DATA into a string appropriate for a filename." 17 | (substring (sha1 (prin1-to-string data)) 0 8)) 18 | 19 | (cl-defun magithub-mock-ghub-request (method resource &optional params 20 | &key query payload headers unpaginate 21 | noerror reader username auth host) 22 | "Mock a call to the GitHub API. 23 | 24 | If the call has not been mocked and the AUTOTEST environment 25 | variable is not set, offer to save a snapshot of the real API's 26 | response." 27 | (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)" 28 | method resource params query payload headers unpaginate noerror reader username auth host) 29 | (when (not (magithub-online-p)) 30 | (error "Did not respect online/offline")) 31 | (let* ((parts (cdr (s-split "/" resource))) 32 | (directory (mapconcat (lambda (s) (concat s ".d")) 33 | (butlast parts) "/")) 34 | (filename (magithub-in-test-dir 35 | (format "mock-data/%s/%s/%s.%s" 36 | (downcase method) 37 | directory 38 | (car (last parts)) 39 | (magithub-mock-data-crunch 40 | (list method resource params query payload headers 41 | unpaginate noerror reader username auth host)))))) 42 | (if (file-readable-p filename) 43 | (prog1 (with-temp-buffer 44 | (insert-file-contents-literally filename) 45 | (read (current-buffer))) 46 | (message "Found %S" filename)) 47 | (message "Did not find %S" filename) 48 | (if (and (not (getenv "AUTOTEST")) 49 | (y-or-n-p (format "Request not mocked; mock now?"))) 50 | (progn 51 | (make-directory directory t) 52 | (let ((real-data (ghub-request method resource params 53 | :query query 54 | :payload payload 55 | :headers headers 56 | :unpaginate unpaginate 57 | :noerror noerror 58 | :reader reader 59 | :username username 60 | :auth auth 61 | :host host))) 62 | (pp-display-expression real-data "*GitHub API Response*") 63 | (if (y-or-n-p "API response displayed; is this ok?") 64 | (with-temp-buffer 65 | (insert (pp-to-string real-data)) 66 | (write-file filename) 67 | (message "Wrote %s" filename)) 68 | (error "API response rejected")))) 69 | (error "Unmocked test!"))))) 70 | --------------------------------------------------------------------------------