mirror of https://github.com/Chizi123/.emacs.d.git

Chizi123
2018-11-17 c4001ccd1864293b64aa37d83a9d9457eb875e70
commit | author | age
5cb5f7 1 ;;; git-commit.el --- Edit Git commit messages  -*- lexical-binding: t; -*-
C 2
3 ;; Copyright (C) 2010-2018  The Magit Project Contributors
4 ;;
5 ;; You should have received a copy of the AUTHORS.md file which
6 ;; lists all contributors.  If not, see http://magit.vc/authors.
7
8 ;; Authors: Jonas Bernoulli <jonas@bernoul.li>
9 ;;    Sebastian Wiesner <lunaryorn@gmail.com>
10 ;;    Florian Ragwitz <rafl@debian.org>
11 ;;    Marius Vollmer <marius.vollmer@gmail.com>
12 ;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
13
14 ;; Package-Requires: ((emacs "25.1") (dash "20180910") (with-editor "20181103"))
15 ;; Package-Version: 20181116.1408
16 ;; Keywords: git tools vc
17 ;; Homepage: https://github.com/magit/magit
18
19 ;; This file is not part of GNU Emacs.
20
21 ;; This file is free software; you can redistribute it and/or modify
22 ;; it under the terms of the GNU General Public License as published by
23 ;; the Free Software Foundation; either version 3, or (at your option)
24 ;; any later version.
25
26 ;; This file is distributed in the hope that it will be useful,
27 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
28 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
29 ;; GNU General Public License for more details.
30
31 ;; You should have received a copy of the GNU General Public License
32 ;; along with this file.  If not, see <http://www.gnu.org/licenses/>.
33
34 ;;; Commentary:
35
36 ;; This package assists the user in writing good Git commit messages.
37
38 ;; While Git allows for the message to be provided on the command
39 ;; line, it is preferable to tell Git to create the commit without
40 ;; actually passing it a message.  Git then invokes the `$GIT_EDITOR'
41 ;; (or if that is undefined `$EDITOR') asking the user to provide the
42 ;; message by editing the file ".git/COMMIT_EDITMSG" (or another file
43 ;; in that directory, e.g. ".git/MERGE_MSG" for merge commits).
44
45 ;; When `global-git-commit-mode' is enabled, which it is by default,
46 ;; then opening such a file causes the features described below, to
47 ;; be enabled in that buffer.  Normally this would be done using a
48 ;; major-mode but to allow the use of any major-mode, as the user sees
49 ;; fit, it is done here by running a setup function, which among other
50 ;; things turns on the preferred major-mode, by default `text-mode'.
51
52 ;; Git waits for the `$EDITOR' to finish and then either creates the
53 ;; commit using the contents of the file as commit message, or, if the
54 ;; editor process exited with a non-zero exit status, aborts without
55 ;; creating a commit.  Unfortunately Emacsclient (which is what Emacs
56 ;; users should be using as `$EDITOR' or at least as `$GIT_EDITOR')
57 ;; does not differentiate between "successfully" editing a file and
58 ;; aborting; not out of the box that is.
59
60 ;; By making use of the `with-editor' package this package provides
61 ;; both ways of finish an editing session.  In either case the file
62 ;; is saved, but Emacseditor's exit code differs.
63 ;;
64 ;;   C-c C-c  Finish the editing session successfully by returning
65 ;;            with exit code 0.  Git then creates the commit using
66 ;;            the message it finds in the file.
67 ;;
68 ;;   C-c C-k  Aborts the edit editing session by returning with exit
69 ;;            code 1.  Git then aborts the commit.
70
71 ;; Aborting the commit does not cause the message to be lost, but
72 ;; relying solely on the file not being tampered with is risky.  This
73 ;; package additionally stores all aborted messages for the duration
74 ;; of the current session (i.e. until you close Emacs).  To get back
75 ;; an aborted message use M-p and M-n while editing a message.
76 ;;
77 ;;   M-p      Replace the buffer contents with the previous message
78 ;;            from the message ring.  Of course only after storing
79 ;;            the current content there too.
80 ;;
81 ;;   M-n      Replace the buffer contents with the next message from
82 ;;            the message ring, after storing the current content.
83
84 ;; Some support for pseudo headers as used in some projects is
85 ;; provided by these commands:
86 ;;
87 ;;   C-c C-s  Insert a Signed-off-by header.
88 ;;   C-c C-a  Insert a Acked-by header.
89 ;;   C-c C-m  Insert a Modified-by header.
90 ;;   C-c C-t  Insert a Tested-by header.
91 ;;   C-c C-r  Insert a Reviewed-by header.
92 ;;   C-c C-o  Insert a Cc header.
93 ;;   C-c C-p  Insert a Reported-by header.
94 ;;   C-c M-s  Insert a Suggested-by header.
95
96 ;; When Git requests a commit message from the user, it does so by
97 ;; having her edit a file which initially contains some comments,
98 ;; instructing her what to do, and providing useful information, such
99 ;; as which files were modified.  These comments, even when left
100 ;; intact by the user, do not become part of the commit message.  This
101 ;; package ensures these comments are propertizes as such and further
102 ;; prettifies them by using different faces for various parts, such as
103 ;; files.
104
105 ;; Finally this package highlights style errors, like lines that are
106 ;; too long, or when the second line is not empty.  It may even nag
107 ;; you when you attempt to finish the commit without having fixed
108 ;; these issues.  The style checks and many other settings can easily
109 ;; be configured:
110 ;;
111 ;;   M-x customize-group RET git-commit RET
112
113 ;;; Code:
114 ;;;; Dependencies
115
116 (require 'dash)
117 (require 'log-edit)
118 (require 'magit-git nil t)
119 (require 'magit-utils nil t)
120 (require 'ring)
121 (require 'server)
122 (require 'with-editor)
123
124 (eval-when-compile (require 'recentf))
125
126 ;;;; Declarations
127
128 (defvar diff-default-read-only)
129 (defvar flyspell-generic-check-word-predicate)
130 (defvar font-lock-beg)
131 (defvar font-lock-end)
132
133 (declare-function magit-expand-git-file-name "magit-git" (filename))
134 (declare-function magit-list-local-branch-names "magit-git" ())
135 (declare-function magit-list-remote-branch-names "magit-git"
136                   (&optional remote relative))
137
138 ;;; Options
139 ;;;; Variables
140
141 (defgroup git-commit nil
142   "Edit Git commit messages."
143   :prefix "git-commit-"
144   :link '(info-link "(magit)Editing Commit Messages")
145   :group 'tools)
146
147 ;;;###autoload
148 (define-minor-mode global-git-commit-mode
149   "Edit Git commit messages.
150 This global mode arranges for `git-commit-setup' to be called
151 when a Git commit message file is opened.  That usually happens
152 when Git uses the Emacsclient as $GIT_EDITOR to have the user
153 provide such a commit message."
154   :group 'git-commit
155   :type 'boolean
156   :global t
157   :init-value t
158   :initialize (lambda (symbol exp)
159                 (custom-initialize-default symbol exp)
160                 (when global-git-commit-mode
161                   (add-hook 'find-file-hook 'git-commit-setup-check-buffer)))
162   (if global-git-commit-mode
163       (add-hook  'find-file-hook 'git-commit-setup-check-buffer)
164     (remove-hook 'find-file-hook 'git-commit-setup-check-buffer)))
165
166 (defcustom git-commit-major-mode 'text-mode
167   "Major mode used to edit Git commit messages.
168 The major mode configured here is turned on by the minor mode
169 `git-commit-mode'."
170   :group 'git-commit
171   :type '(choice (function-item text-mode)
172                  (const :tag "No major mode")))
173
174 (defcustom git-commit-setup-hook
175   '(git-commit-save-message
176     git-commit-setup-changelog-support
177     git-commit-turn-on-auto-fill
178     git-commit-propertize-diff
179     bug-reference-mode
180     with-editor-usage-message)
181   "Hook run at the end of `git-commit-setup'."
182   :group 'git-commit
183   :type 'hook
184   :get (and (featurep 'magit-utils) 'magit-hook-custom-get)
185   :options '(git-commit-save-message
186              git-commit-setup-changelog-support
187              git-commit-turn-on-auto-fill
188              git-commit-turn-on-flyspell
189              git-commit-propertize-diff
190              bug-reference-mode
191              with-editor-usage-message))
192
193 (defcustom git-commit-post-finish-hook nil
194   "Hook run after the user finished writing a commit message.
195
196 \\<with-editor-mode-map>\
197 This hook is only run after pressing \\[with-editor-finish] in a buffer used
198 to edit a commit message.  If a commit is created without the
199 user typing a message into a buffer, then this hook is not run.
200
201 This hook is not run until the new commit has been created.  If
202 doing so takes Git longer than one second, then this hook isn't
203 run at all.  For certain commands such as `magit-rebase-continue'
204 this hook is never run because doing so would lead to a race
205 condition.
206
207 Also see `magit-post-commit-hook'."
208   :group 'git-commit
209   :type 'hook
210   :get (and (featurep 'magit-utils) 'magit-hook-custom-get))
211
212 (defcustom git-commit-finish-query-functions
213   '(git-commit-check-style-conventions)
214   "List of functions called to query before performing commit.
215
216 The commit message buffer is current while the functions are
217 called.  If any of them returns nil, then the commit is not
218 performed and the buffer is not killed.  The user should then
219 fix the issue and try again.
220
221 The functions are called with one argument.  If it is non-nil,
222 then that indicates that the user used a prefix argument to
223 force finishing the session despite issues.  Functions should
224 usually honor this wish and return non-nil."
225   :options '(git-commit-check-style-conventions)
226   :type 'hook
227   :group 'git-commit)
228
229 (defcustom git-commit-style-convention-checks '(non-empty-second-line)
230   "List of checks performed by `git-commit-check-style-conventions'.
231 Valid members are `non-empty-second-line' and `overlong-summary-line'.
232 That function is a member of `git-commit-finish-query-functions'."
233   :options '(non-empty-second-line overlong-summary-line)
234   :type '(list :convert-widget custom-hook-convert-widget)
235   :group 'git-commit)
236
237 (defcustom git-commit-summary-max-length 68
238   "Column beyond which characters in the summary lines are highlighted.
239
240 The highlighting indicates that the summary is getting too long
241 by some standards.  It does in no way imply that going over the
242 limit a few characters or in some cases even many characters is
243 anything that deserves shaming.  It's just a friendly reminder
244 that if you can make the summary shorter, then you might want
245 to consider doing so."
246   :group 'git-commit
247   :safe 'numberp
248   :type 'number)
249
250 (defcustom git-commit-fill-column nil
251   "Override `fill-column' in commit message buffers.
252
253 If this is non-nil, then it should be an integer.  If that is the
254 case and the buffer-local value of `fill-column' is not already
255 set by the time `git-commit-turn-on-auto-fill' is called as a
256 member of `git-commit-setup-hook', then that function sets the
257 buffer-local value of `fill-column' to the value of this option.
258
259 This option exists mostly for historic reasons.  If you are not
260 already using it, then you probably shouldn't start doing so."
261   :group 'git-commit
262   :safe 'numberp
263   :type '(choice (const :tag "use regular fill-column")
264                  number))
265
266 (make-obsolete-variable 'git-commit-fill-column 'fill-column
267                         "Magit 2.11.0" 'set)
268
269 (defcustom git-commit-known-pseudo-headers
270   '("Signed-off-by" "Acked-by" "Modified-by" "Cc"
271     "Suggested-by" "Reported-by" "Tested-by" "Reviewed-by")
272   "A list of Git pseudo headers to be highlighted."
273   :group 'git-commit
274   :safe (lambda (val) (and (listp val) (-all-p 'stringp val)))
275   :type '(repeat string))
276
277 ;;;; Faces
278
279 (defgroup git-commit-faces nil
280   "Faces used for highlighting Git commit messages."
281   :prefix "git-commit-"
282   :group 'git-commit
283   :group 'faces)
284
285 (defface git-commit-summary
286   '((t :inherit font-lock-type-face))
287   "Face used for the summary in commit messages."
288   :group 'git-commit-faces)
289
290 (defface git-commit-overlong-summary
291   '((t :inherit font-lock-warning-face))
292   "Face used for the tail of overlong commit message summaries."
293   :group 'git-commit-faces)
294
295 (defface git-commit-nonempty-second-line
296   '((t :inherit font-lock-warning-face))
297   "Face used for non-whitespace on the second line of commit messages."
298   :group 'git-commit-faces)
299
300 (defface git-commit-note
301   '((t :inherit font-lock-string-face))
302   "Face used for notes in commit messages."
303   :group 'git-commit-faces)
304
305 (defface git-commit-pseudo-header
306   '((t :inherit font-lock-string-face))
307   "Face used for pseudo headers in commit messages."
308   :group 'git-commit-faces)
309
310 (defface git-commit-known-pseudo-header
311   '((t :inherit font-lock-keyword-face))
312   "Face used for the keywords of known pseudo headers in commit messages."
313   :group 'git-commit-faces)
314
315 (defface git-commit-comment-branch-local
316   (if (featurep 'magit)
317       '((t :inherit magit-branch-local))
318     '((t :inherit font-lock-variable-name-face)))
319   "Face used for names of local branches in commit message comments."
320   :group 'git-commit-faces)
321
322 (define-obsolete-face-alias 'git-commit-comment-branch
323   'git-commit-comment-branch-local "Git-Commit 2.12.0")
324
325 (defface git-commit-comment-branch-remote
326   (if (featurep 'magit)
327       '((t :inherit magit-branch-remote))
328     '((t :inherit font-lock-variable-name-face)))
329   "Face used for names of remote branches in commit message comments.
330 This is only used if Magit is available."
331   :group 'git-commit-faces)
332
333 (defface git-commit-comment-detached
334   '((t :inherit git-commit-comment-branch-local))
335   "Face used for detached `HEAD' in commit message comments."
336   :group 'git-commit-faces)
337
338 (defface git-commit-comment-heading
339   '((t :inherit git-commit-known-pseudo-header))
340   "Face used for headings in commit message comments."
341   :group 'git-commit-faces)
342
343 (defface git-commit-comment-file
344   '((t :inherit git-commit-pseudo-header))
345   "Face used for file names in commit message comments."
346   :group 'git-commit-faces)
347
348 (defface git-commit-comment-action
349   '((t :inherit bold))
350   "Face used for actions in commit message comments."
351   :group 'git-commit-faces)
352
353 ;;; Keymap
354
355 (defvar git-commit-mode-map
356   (let ((map (make-sparse-keymap)))
357     (cond ((featurep 'jkl)
358            (define-key map (kbd "C-M-i") 'git-commit-prev-message)
359            (define-key map (kbd "C-M-k") 'git-commit-next-message))
360           (t
361            (define-key map (kbd "M-p") 'git-commit-prev-message)
362            (define-key map (kbd "M-n") 'git-commit-next-message)
363            ;; Old bindings to avoid confusion
364            (define-key map (kbd "C-c C-x a") 'git-commit-ack)
365            (define-key map (kbd "C-c C-x i") 'git-commit-suggested)
366            (define-key map (kbd "C-c C-x m") 'git-commit-modified)
367            (define-key map (kbd "C-c C-x o") 'git-commit-cc)
368            (define-key map (kbd "C-c C-x p") 'git-commit-reported)
369            (define-key map (kbd "C-c C-x r") 'git-commit-review)
370            (define-key map (kbd "C-c C-x s") 'git-commit-signoff)
371            (define-key map (kbd "C-c C-x t") 'git-commit-test)))
372     (define-key map (kbd "C-c C-a") 'git-commit-ack)
373     (define-key map (kbd "C-c C-i") 'git-commit-suggested)
374     (define-key map (kbd "C-c C-m") 'git-commit-modified)
375     (define-key map (kbd "C-c C-o") 'git-commit-cc)
376     (define-key map (kbd "C-c C-p") 'git-commit-reported)
377     (define-key map (kbd "C-c C-r") 'git-commit-review)
378     (define-key map (kbd "C-c C-s") 'git-commit-signoff)
379     (define-key map (kbd "C-c C-t") 'git-commit-test)
380     (define-key map (kbd "C-c M-s") 'git-commit-save-message)
381     map)
382   "Key map used by `git-commit-mode'.")
383
384 ;;; Menu
385
386 (require 'easymenu)
387 (easy-menu-define git-commit-mode-menu git-commit-mode-map
388   "Git Commit Mode Menu"
389   '("Commit"
390     ["Previous" git-commit-prev-message t]
391     ["Next" git-commit-next-message t]
392     "-"
393     ["Ack" git-commit-ack :active t
394      :help "Insert an 'Acked-by' header"]
395     ["Sign-Off" git-commit-signoff :active t
396      :help "Insert a 'Signed-off-by' header"]
397     ["Modified-by" git-commit-modified :active t
398      :help "Insert a 'Modified-by' header"]
399     ["Tested-by" git-commit-test :active t
400      :help "Insert a 'Tested-by' header"]
401     ["Reviewed-by" git-commit-review :active t
402      :help "Insert a 'Reviewed-by' header"]
403     ["CC" git-commit-cc t
404      :help "Insert a 'Cc' header"]
405     ["Reported" git-commit-reported :active t
406      :help "Insert a 'Reported-by' header"]
407     ["Suggested" git-commit-suggested t
408      :help "Insert a 'Suggested-by' header"]
409     "-"
410     ["Save" git-commit-save-message t]
411     ["Cancel" with-editor-cancel t]
412     ["Commit" with-editor-finish t]))
413
414 ;;; Hooks
415
416 ;;;###autoload
417 (defconst git-commit-filename-regexp "/\\(\
418 \\(\\(COMMIT\\|NOTES\\|PULLREQ\\|TAG\\)_EDIT\\|MERGE_\\|\\)MSG\
419 \\|\\(BRANCH\\|EDIT\\)_DESCRIPTION\\)\\'")
420
421 (eval-after-load 'recentf
422   '(add-to-list 'recentf-exclude git-commit-filename-regexp))
423
424 (add-to-list 'with-editor-file-name-history-exclude git-commit-filename-regexp)
425
426 (defun git-commit-setup-font-lock-in-buffer ()
427   (and buffer-file-name
428        (string-match-p git-commit-filename-regexp buffer-file-name)
429        (git-commit-setup-font-lock)))
430
431 (add-hook 'after-change-major-mode-hook 'git-commit-setup-font-lock-in-buffer)
432
433 ;;;###autoload
434 (defun git-commit-setup-check-buffer ()
435   (and buffer-file-name
436        (string-match-p git-commit-filename-regexp buffer-file-name)
437        (git-commit-setup)))
438
439 (defvar git-commit-mode)
440
441 (defun git-commit-file-not-found ()
442   ;; cygwin git will pass a cygwin path (/cygdrive/c/foo/.git/...),
443   ;; try to handle this in window-nt Emacs.
444   (--when-let
445       (and (or (string-match-p git-commit-filename-regexp buffer-file-name)
446                (and (boundp 'git-rebase-filename-regexp)
447                     (string-match-p git-rebase-filename-regexp
448                                     buffer-file-name)))
449            (not (file-accessible-directory-p
450                  (file-name-directory buffer-file-name)))
451            (if (require 'magit-git nil t)
452                ;; Emacs prepends a "c:".
453                (magit-expand-git-file-name (substring buffer-file-name 2))
454              ;; Fallback if we can't load `magit-git'.
455              (and (string-match "\\`[a-z]:/\\(cygdrive/\\)?\\([a-z]\\)/\\(.*\\)"
456                                 buffer-file-name)
457                   (concat (match-string 2 buffer-file-name) ":/"
458                           (match-string 3 buffer-file-name)))))
459     (when (file-accessible-directory-p (file-name-directory it))
460       (let ((inhibit-read-only t))
461         (insert-file-contents it t)
462         t))))
463
464 (when (eq system-type 'windows-nt)
465   (add-hook 'find-file-not-found-functions #'git-commit-file-not-found))
466
467 ;;;###autoload
468 (defun git-commit-setup ()
469   ;; Pretend that git-commit-mode is a major-mode,
470   ;; so that directory-local settings can be used.
471   (let ((default-directory
472           (if (or (file-exists-p ".dir-locals.el")
473                   (not (fboundp 'magit-toplevel)))
474               default-directory
475             ;; When $GIT_DIR/.dir-locals.el doesn't exist,
476             ;; fallback to $GIT_WORK_TREE/.dir-locals.el,
477             ;; because the maintainer can use the latter
478             ;; to enforce conventions, while s/he has no
479             ;; control over the former.
480             (and (fboundp 'magit-toplevel) ; silence byte-compiler
481                  (magit-toplevel)))))
482     (let ((buffer-file-name nil)         ; trick hack-dir-local-variables
483           (major-mode 'git-commit-mode)) ; trick dir-locals-collect-variables
484       (hack-dir-local-variables)
485       (hack-local-variables-apply)))
486   (when git-commit-major-mode
487     (let ((auto-mode-alist (list (cons (concat "\\`"
488                                                (regexp-quote buffer-file-name)
489                                                "\\'")
490                                        git-commit-major-mode)))
491           ;; The major-mode hook might want to consult these minor
492           ;; modes, while the minor-mode hooks might want to consider
493           ;; the major mode.
494           (git-commit-mode t)
495           (with-editor-mode t))
496       (normal-mode t)))
497   (setq with-editor-show-usage nil)
498   (unless with-editor-mode
499     ;; Maybe already enabled when using `shell-command' or an Emacs shell.
500     (with-editor-mode 1))
501   (add-hook 'with-editor-finish-query-functions
502             'git-commit-finish-query-functions nil t)
503   (add-hook 'with-editor-pre-finish-hook
504             'git-commit-save-message nil t)
505   (add-hook 'with-editor-pre-cancel-hook
506             'git-commit-save-message nil t)
507   (when (and (fboundp 'magit-rev-parse)
508              (not (memq last-command
509                         '(magit-sequencer-continue
510                           magit-sequencer-skip
511                           magit-am-continue
512                           magit-am-skip
513                           magit-rebase-continue
514                           magit-rebase-skip))))
515     (add-hook 'with-editor-post-finish-hook
516               (apply-partially 'git-commit-run-post-finish-hook
517                                (magit-rev-parse "HEAD"))
518               nil t)
519     (when (fboundp 'magit-wip-maybe-add-commit-hook)
520       (magit-wip-maybe-add-commit-hook)))
521   (setq with-editor-cancel-message
522         'git-commit-cancel-message)
523   (make-local-variable 'log-edit-comment-ring-index)
524   (git-commit-mode 1)
525   (git-commit-setup-font-lock)
526   (when (boundp 'save-place)
527     (setq save-place nil))
528   (save-excursion
529     (goto-char (point-min))
530     (when (looking-at "\\`\\(\\'\\|\n[^\n]\\)")
531       (open-line 1)))
532   (run-hooks 'git-commit-setup-hook)
533   (set-buffer-modified-p nil))
534
535 (defun git-commit-run-post-finish-hook (previous)
536   (when git-commit-post-finish-hook
537     (cl-block nil
538       (let ((break (time-add (current-time)
539                              (seconds-to-time 1))))
540         (while (equal (magit-rev-parse "HEAD") previous)
541           (if (time-less-p (current-time) break)
542               (sit-for 0.01)
543             (message "No commit created after 1 second.  Not running %s."
544                      'git-commit-post-finish-hook)
545             (cl-return))))
546       (run-hooks 'git-commit-post-finish-hook))))
547
548 (define-minor-mode git-commit-mode
549   "Auxiliary minor mode used when editing Git commit messages.
550 This mode is only responsible for setting up some key bindings.
551 Don't use it directly, instead enable `global-git-commit-mode'."
552   :lighter "")
553
554 (put 'git-commit-mode 'permanent-local t)
555
556 (defun git-commit-setup-changelog-support ()
557   "Treat ChangeLog entries as paragraphs."
558   (setq-local paragraph-start (concat paragraph-start "\\|\\*\\|(")))
559
560 (defun git-commit-turn-on-auto-fill ()
561   "Unconditionally turn on Auto Fill mode.
562 If `git-commit-fill-column' is non-nil, and `fill-column'
563 doesn't already have a buffer-local value, then set that
564 to `git-commit-fill-column'."
565   (when (and (numberp git-commit-fill-column)
566              (not (local-variable-p 'fill-column)))
567     (setq fill-column git-commit-fill-column))
568   (setq-local comment-auto-fill-only-comments nil)
569   (turn-on-auto-fill))
570
571 (defun git-commit-turn-on-flyspell ()
572   "Unconditionally turn on Flyspell mode.
573 Also prevent comments from being checked and
574 finally check current non-comment text."
575   (require 'flyspell)
576   (turn-on-flyspell)
577   (setq flyspell-generic-check-word-predicate
578         'git-commit-flyspell-verify)
579   (let ((end)
580         (comment-start-regex (format "^\\(%s\\|$\\)" comment-start)))
581     (save-excursion
582       (goto-char (point-max))
583       (while (and (not (bobp)) (looking-at comment-start-regex))
584         (forward-line -1))
585       (unless (looking-at comment-start-regex)
586         (forward-line))
587       (setq end (point)))
588     (flyspell-region (point-min) end)))
589
590 (defun git-commit-flyspell-verify ()
591   (not (= (char-after (line-beginning-position))
592           (aref comment-start 0))))
593
594 (defun git-commit-finish-query-functions (force)
595   (run-hook-with-args-until-failure
596    'git-commit-finish-query-functions force))
597
598 (defun git-commit-check-style-conventions (force)
599   "Check for violations of certain basic style conventions.
600
601 For each violation ask the user if she wants to proceed anyway.
602 Option `git-commit-check-style-conventions' controls which
603 conventions are checked."
604   (or force
605       (save-excursion
606         (goto-char (point-min))
607         (re-search-forward (git-commit-summary-regexp) nil t)
608         (if (equal (match-string 1) "")
609             t ; Just try; we don't know whether --allow-empty-message was used.
610           (and (or (not (memq 'overlong-summary-line
611                               git-commit-style-convention-checks))
612                    (equal (match-string 2) "")
613                    (y-or-n-p "Summary line is too long.  Commit anyway? "))
614                (or (not (memq 'non-empty-second-line
615                               git-commit-style-convention-checks))
616                    (not (match-string 3))
617                    (y-or-n-p "Second line is not empty.  Commit anyway? ")))))))
618
619 (defun git-commit-cancel-message ()
620   (message
621    (concat "Commit canceled"
622            (and (memq 'git-commit-save-message with-editor-pre-cancel-hook)
623                 ".  Message saved to `log-edit-comment-ring'"))))
624
625 ;;; History
626
627 (defun git-commit-prev-message (arg)
628   "Cycle backward through message history, after saving current message.
629 With a numeric prefix ARG, go back ARG comments."
630   (interactive "*p")
631   (when (and (git-commit-save-message) (> arg 0))
632     (setq log-edit-comment-ring-index
633           (log-edit-new-comment-index
634            arg (ring-length log-edit-comment-ring))))
635   (save-restriction
636     (goto-char (point-min))
637     (narrow-to-region (point)
638                       (if (re-search-forward (concat "^" comment-start) nil t)
639                           (max 1 (- (point) 2))
640                         (point-max)))
641     (log-edit-previous-comment arg)))
642
643 (defun git-commit-next-message (arg)
644   "Cycle forward through message history, after saving current message.
645 With a numeric prefix ARG, go forward ARG comments."
646   (interactive "*p")
647   (git-commit-prev-message (- arg)))
648
649 (defun git-commit-save-message ()
650   "Save current message to `log-edit-comment-ring'."
651   (interactive)
652   (--when-let (git-commit-buffer-message)
653     (unless (ring-member log-edit-comment-ring it)
654       (ring-insert log-edit-comment-ring it))))
655
656 (defun git-commit-buffer-message ()
657   (let ((flush (concat "^" comment-start))
658         (str (buffer-substring-no-properties (point-min) (point-max))))
659     (with-temp-buffer
660       (insert str)
661       (goto-char (point-min))
662       (when (re-search-forward (concat flush " -+ >8 -+$") nil t)
663         (delete-region (point-at-bol) (point-max)))
664       (goto-char (point-min))
665       (flush-lines flush)
666       (goto-char (point-max))
667       (unless (eq (char-before) ?\n)
668         (insert ?\n))
669       (setq str (buffer-string)))
670     (unless (string-match "\\`[ \t\n\r]*\\'" str)
671       (when (string-match "\\`\n\\{2,\\}" str)
672         (setq str (replace-match "\n" t t str)))
673       (when (string-match "\n\\{2,\\}\\'" str)
674         (setq str (replace-match "\n" t t str)))
675       str)))
676
677 ;;; Headers
678
679 (defun git-commit-ack (name mail)
680   "Insert a header acknowledging that you have looked at the commit."
681   (interactive (git-commit-self-ident))
682   (git-commit-insert-header "Acked-by" name mail))
683
684 (defun git-commit-modified (name mail)
685   "Insert a header to signal that you have modified the commit."
686   (interactive (git-commit-self-ident))
687   (git-commit-insert-header "Modified-by" name mail))
688
689 (defun git-commit-review (name mail)
690   "Insert a header acknowledging that you have reviewed the commit."
691   (interactive (git-commit-self-ident))
692   (git-commit-insert-header "Reviewed-by" name mail))
693
694 (defun git-commit-signoff (name mail)
695   "Insert a header to sign off the commit."
696   (interactive (git-commit-self-ident))
697   (git-commit-insert-header "Signed-off-by" name mail))
698
699 (defun git-commit-test (name mail)
700   "Insert a header acknowledging that you have tested the commit."
701   (interactive (git-commit-self-ident))
702   (git-commit-insert-header "Tested-by" name mail))
703
704 (defun git-commit-cc (name mail)
705   "Insert a header mentioning someone who might be interested."
706   (interactive (git-commit-read-ident))
707   (git-commit-insert-header "Cc" name mail))
708
709 (defun git-commit-reported (name mail)
710   "Insert a header mentioning the person who reported the issue."
711   (interactive (git-commit-read-ident))
712   (git-commit-insert-header "Reported-by" name mail))
713
714 (defun git-commit-suggested (name mail)
715   "Insert a header mentioning the person who suggested the change."
716   (interactive (git-commit-read-ident))
717   (git-commit-insert-header "Suggested-by" name mail))
718
719 (defun git-commit-self-ident ()
720   (list (or (getenv "GIT_AUTHOR_NAME")
721             (getenv "GIT_COMMITTER_NAME")
722             (ignore-errors (car (process-lines "git" "config" "user.name")))
723             user-full-name
724             (read-string "Name: "))
725         (or (getenv "GIT_AUTHOR_EMAIL")
726             (getenv "GIT_COMMITTER_EMAIL")
727             (getenv "EMAIL")
728             (ignore-errors (car (process-lines "git" "config" "user.email")))
729             (read-string "Email: "))))
730
731 (defun git-commit-read-ident ()
732   (list (read-string "Name: ")
733         (read-string "Email: ")))
734
735 (defun git-commit-insert-header (header name email)
736   (setq header (format "%s: %s <%s>" header name email))
737   (save-excursion
738     (goto-char (point-max))
739     (cond ((re-search-backward "^[-a-zA-Z]+: [^<]+? <[^>]+>" nil t)
740            (end-of-line)
741            (insert ?\n header)
742            (unless (= (char-after) ?\n)
743              (insert ?\n)))
744           (t
745            (while (re-search-backward (concat "^" comment-start) nil t))
746            (unless (looking-back "\n\n" nil)
747              (insert ?\n))
748            (insert header ?\n)))
749     (unless (or (eobp) (= (char-after) ?\n))
750       (insert ?\n))))
751
752 ;;; Font-Lock
753
754 (defun git-commit-summary-regexp ()
755   (concat
756    ;; Leading empty lines and comments
757    (format "\\`\\(?:^\\(?:\\s-*\\|%s.*\\)\n\\)*" comment-start)
758    ;; Summary line
759    (format "\\(.\\{0,%d\\}\\)\\(.*\\)" git-commit-summary-max-length)
760    ;; Non-empty non-comment second line
761    (format "\\(?:\n%s\\|\n\\(.+\\)\\)?" comment-start)))
762
763 (defun git-commit-extend-region-summary-line ()
764   "Identify the multiline summary-regexp construct.
765 Added to `font-lock-extend-region-functions'."
766   (save-excursion
767     (save-match-data
768       (goto-char (point-min))
769       (when (looking-at (git-commit-summary-regexp))
770         (let ((summary-beg (match-beginning 0))
771               (summary-end (match-end 0)))
772           (when (or (< summary-beg font-lock-beg summary-end)
773                     (< summary-beg font-lock-end summary-end))
774             (setq font-lock-beg (min font-lock-beg summary-beg))
775             (setq font-lock-end (max font-lock-end summary-end))))))))
776
777 (defvar-local git-commit--branch-name-regexp nil)
778
779 (defconst git-commit-comment-headings
780   '("Changes to be committed:"
781     "Untracked files:"
782     "Changed but not updated:"
783     "Changes not staged for commit:"
784     "Unmerged paths:"
785     "Author:"
786     "Date:"))
787
788 (defconst git-commit-font-lock-keywords-1
789   '(;; Pseudo headers
790     (eval . `(,(format "^\\(%s:\\)\\( .*\\)"
791                        (regexp-opt git-commit-known-pseudo-headers))
792               (1 'git-commit-known-pseudo-header)
793               (2 'git-commit-pseudo-header)))
794     ("^[-a-zA-Z]+: [^<]+? <[^>]+>"
795      (0 'git-commit-pseudo-header))
796     ;; Summary
797     (eval . `(,(git-commit-summary-regexp)
798               (1 'git-commit-summary)))
799     ;; - Note (overrides summary)
800     ("\\[.+?\\]"
801      (0 'git-commit-note t))
802     ;; - Non-empty second line (overrides summary and note)
803     (eval . `(,(git-commit-summary-regexp)
804               (2 'git-commit-overlong-summary t t)
805               (3 'git-commit-nonempty-second-line t t)))))
806
807 (defconst git-commit-font-lock-keywords-2
808   `(,@git-commit-font-lock-keywords-1
809     ;; Comments
810     (eval . `(,(format "^%s.*" comment-start)
811               (0 'font-lock-comment-face)))
812     (eval . `(,(format "^%s On branch \\(.*\\)" comment-start)
813               (1 'git-commit-comment-branch-local t)))
814     (eval . `(,(format "^%s \\(HEAD\\) detached at" comment-start)
815               (1 'git-commit-comment-detached t)))
816     (eval . `(,(format "^%s %s" comment-start
817                        (regexp-opt git-commit-comment-headings t))
818               (1 'git-commit-comment-heading t)))
819     (eval . `(,(format "^%s\t\\(?:\\([^:\n]+\\):\\s-+\\)?\\(.*\\)" comment-start)
820               (1 'git-commit-comment-action t t)
821               (2 'git-commit-comment-file t)))))
822
823 (defconst git-commit-font-lock-keywords-3
824   `(,@git-commit-font-lock-keywords-2
825     ;; More comments
826     (eval
827      ;; Your branch is ahead of 'master' by 3 commits.
828      ;; Your branch is behind 'master' by 2 commits, and can be fast-forwarded.
829      . `(,(format
830            "^%s Your branch is \\(?:ahead\\|behind\\) of '%s' by \\([0-9]*\\)"
831            comment-start git-commit--branch-name-regexp)
832          (1 'git-commit-comment-branch-local t)
833          (2 'git-commit-comment-branch-remote t)
834          (3 'bold t)))
835     (eval
836      ;; Your branch is up to date with 'master'.
837      ;; Your branch and 'master' have diverged,
838      . `(,(format
839            "^%s Your branch \\(?:is up-to-date with\\|and\\) '%s'"
840            comment-start git-commit--branch-name-regexp)
841          (1 'git-commit-comment-branch-local t)
842          (2 'git-commit-comment-branch-remote t)))
843     (eval
844      ;; and have 1 and 2 different commits each, respectively.
845      . `(,(format
846            "^%s and have \\([0-9]*\\) and \\([0-9]*\\) commits each"
847            comment-start)
848          (1 'bold t)
849          (2 'bold t)))))
850
851 (defvar git-commit-font-lock-keywords git-commit-font-lock-keywords-2
852   "Font-Lock keywords for Git-Commit mode.")
853
854 (defun git-commit-setup-font-lock ()
855   (let ((table (make-syntax-table (syntax-table))))
856     (when comment-start
857       (modify-syntax-entry (string-to-char comment-start) "." table))
858     (modify-syntax-entry ?#  "." table)
859     (modify-syntax-entry ?\" "." table)
860     (modify-syntax-entry ?\' "." table)
861     (modify-syntax-entry ?`  "." table)
862     (set-syntax-table table))
863   (setq-local comment-start
864               (or (ignore-errors
865                     (car (process-lines "git" "config" "core.commentchar")))
866                   "#"))
867   (setq-local comment-start-skip (format "^%s+[\s\t]*" comment-start))
868   (setq-local comment-end-skip "\n")
869   (setq-local comment-use-syntax nil)
870   (setq-local git-commit--branch-name-regexp
871               (if (and (featurep 'magit-git)
872                        ;; When using cygwin git, we may end up in a
873                        ;; non-existing directory, which would cause
874                        ;; any git calls to signal an error.
875                        (file-accessible-directory-p default-directory))
876                   (progn
877                     ;; Make sure the below functions are available.
878                     (require 'magit)
879                     ;; Font-Lock wants every submatch to succeed,
880                     ;; so also match the empty string.  Do not use
881                     ;; `regexp-quote' because that is slow if there
882                     ;; are thousands of branches outweighing the
883                     ;; benefit of an efficient regep.
884                     (format "\\(\\(?:%s\\)\\|\\)\\(\\(?:%s\\)\\|\\)"
885                             (mapconcat #'identity
886                                        (magit-list-local-branch-names)
887                                        "\\|")
888                             (mapconcat #'identity
889                                        (magit-list-remote-branch-names)
890                                        "\\|")))
891                 "\\([^']*\\)"))
892   (setq-local font-lock-multiline t)
893   (add-hook 'font-lock-extend-region-functions
894             #'git-commit-extend-region-summary-line
895             t t)
896   (font-lock-add-keywords nil git-commit-font-lock-keywords t))
897
898 (defun git-commit-propertize-diff ()
899   (require 'diff-mode)
900   (save-excursion
901     (goto-char (point-min))
902     (when (re-search-forward "^diff --git" nil t)
903       (beginning-of-line)
904       (let ((buffer (current-buffer)))
905         (insert
906          (with-temp-buffer
907            (insert
908             (with-current-buffer buffer
909               (prog1 (buffer-substring-no-properties (point) (point-max))
910                 (delete-region (point) (point-max)))))
911            (let ((diff-default-read-only nil))
912              (diff-mode))
913            (let (font-lock-verbose font-lock-support-mode)
914              (if (fboundp 'font-lock-ensure)
915                  (font-lock-ensure)
916                (with-no-warnings
917                  (font-lock-fontify-buffer))))
918            (let (next (pos (point-min)))
919              (while (setq next (next-single-property-change pos 'face))
920                (put-text-property pos next 'font-lock-face
921                                   (get-text-property pos 'face))
922                (setq pos next))
923              (put-text-property pos (point-max) 'font-lock-face
924                                 (get-text-property pos 'face)))
925            (buffer-string)))))))
926
927 ;;; Elisp Text Mode
928
929 (define-derived-mode git-commit-elisp-text-mode text-mode "ElText"
930   "Major mode for editing commit messages of elisp projects.
931 This is intended for use as `git-commit-major-mode' for projects
932 that expect `symbols' to look like this.  I.e. like they look in
933 Elisp doc-strings, including this one.  Unlike in doc-strings,
934 \"strings\" also look different than the other text."
935   (setq font-lock-defaults '(git-commit-elisp-text-mode-keywords)))
936
937 (defvar git-commit-elisp-text-mode-keywords
938   `((,(concat "[`‘]\\(\\(?:\\sw\\|\\s_\\|\\\\.\\)"
939               lisp-mode-symbol-regexp "\\)['’]")
940      (1 font-lock-constant-face prepend))
941     ("\"[^\"]*\"" (0 font-lock-string-face prepend))))
942
943 ;;; _
944 (provide 'git-commit)
945 ;;; git-commit.el ends here