├── .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 |
17 |
18 | # Magithub
19 |
20 | [](http://melpa.milkbox.net/#/magithub)
21 | [](https://travis-ci.org/vermiculus/magithub)
22 | [](https://gitter.im/vermiculus/magithub)
23 | [](http://melpa-stable.milkbox.net/#/magithub)
24 | [](//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 | 
10 |
11 | ## Submitting a Pull Request
12 |
13 | 
14 |
15 | ## CI Status
16 |
17 | 
18 |
19 | 
20 |
21 | 
22 |
23 | ---
24 |
25 | |
26 | :-------------------------:|:-------------------------:
27 | |
28 | |
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 |
--------------------------------------------------------------------------------