fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 23h ago
1397 lines · 54.7 KB Raw Blame History
1 ;;; git-commit.el --- Edit Git commit messages -*- lexical-binding:t; coding:utf-8 -*-
2
3 ;; Copyright (C) 2008-2026 The Magit Project Contributors
4
5 ;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
6 ;; Sebastian Wiesner <lunaryorn@gmail.com>
7 ;; Florian Ragwitz <rafl@debian.org>
8 ;; Marius Vollmer <marius.vollmer@gmail.com>
9 ;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
10
11 ;; SPDX-License-Identifier: GPL-3.0-or-later
12
13 ;; Magit is free software: you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published
15 ;; by the Free Software Foundation, either version 3 of the License,
16 ;; or (at your option) any later version.
17 ;;
18 ;; Magit is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
22 ;;
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with Magit. If not, see <https://www.gnu.org/licenses/>.
25
26 ;; You should have received a copy of the AUTHORS.md file, which
27 ;; lists all contributors. If not, see https://magit.vc/authors.
28
29 ;;; Commentary:
30
31 ;; This package assists the user in writing good Git commit messages.
32
33 ;; While Git allows for the message to be provided on the command
34 ;; line, it is preferable to tell Git to create the commit without
35 ;; actually passing it a message. Git then invokes the `$GIT_EDITOR'
36 ;; (or if that is undefined `$EDITOR') asking the user to provide the
37 ;; message by editing the file ".git/COMMIT_EDITMSG" (or another file
38 ;; in that directory, e.g., ".git/MERGE_MSG" for merge commits).
39
40 ;; When `global-git-commit-mode' is enabled, which it is by default,
41 ;; then opening such a file causes the features described below, to
42 ;; be enabled in that buffer. Normally this would be done using a
43 ;; major-mode but to allow the use of any major-mode, as the user sees
44 ;; fit, it is done here by running a setup function, which among other
45 ;; things turns on the preferred major-mode, by default `text-mode'.
46
47 ;; Git waits for the `$EDITOR' to finish and then either creates the
48 ;; commit using the contents of the file as commit message, or, if the
49 ;; editor process exited with a non-zero exit status, aborts without
50 ;; creating a commit. Unfortunately Emacsclient (which is what Emacs
51 ;; users should be using as `$EDITOR' or at least as `$GIT_EDITOR')
52 ;; does not differentiate between "successfully" editing a file and
53 ;; aborting; not out of the box that is.
54
55 ;; By making use of the `with-editor' package this package provides
56 ;; both ways of finish an editing session. In either case the file
57 ;; is saved, but Emacseditor's exit code differs.
58 ;;
59 ;; C-c C-c Finish the editing session successfully by returning
60 ;; with exit code 0. Git then creates the commit using
61 ;; the message it finds in the file.
62 ;;
63 ;; C-c C-k Aborts the edit editing session by returning with exit
64 ;; code 1. Git then aborts the commit.
65
66 ;; Aborting the commit does not cause the message to be lost, but
67 ;; relying solely on the file not being tampered with is risky. This
68 ;; package additionally stores all aborted messages for the duration
69 ;; of the current session (i.e., until you close Emacs). To get back
70 ;; an aborted message use M-p and M-n while editing a message.
71 ;;
72 ;; M-p Replace the buffer contents with the previous message
73 ;; from the message ring. Of course only after storing
74 ;; the current content there too.
75 ;;
76 ;; M-n Replace the buffer contents with the next message from
77 ;; the message ring, after storing the current content.
78
79 ;; Support for inserting Git trailers (as described in the manpage
80 ;; git-interpret-trailers(1)) is available.
81 ;;
82 ;; C-c C-i Insert a trailer selected from a transient menu.
83
84 ;; When Git requests a commit message from the user, it does so by
85 ;; having her edit a file which initially contains some comments,
86 ;; instructing her what to do, and providing useful information, such
87 ;; as which files were modified. These comments, even when left
88 ;; intact by the user, do not become part of the commit message. This
89 ;; package ensures these comments are propertizes as such and further
90 ;; prettifies them by using different faces for various parts, such as
91 ;; files.
92
93 ;; Finally this package highlights style errors, like lines that are
94 ;; too long, or when the second line is not empty. It may even nag
95 ;; you when you attempt to finish the commit without having fixed
96 ;; these issues. The style checks and many other settings can easily
97 ;; be configured:
98 ;;
99 ;; M-x customize-group RET git-commit RET
100
101 ;;; Code:
102
103 (require 'magit-git)
104 (require 'magit-mode)
105 (require 'magit-process)
106
107 (require 'log-edit)
108 (require 'ring)
109 (require 'server)
110 (require 'transient)
111 (require 'with-editor)
112
113 (defvar dabbrev--abbrev-char-regexp)
114 (defvar diff-default-read-only)
115 (defvar flyspell-generic-check-word-predicate)
116 (defvar font-lock-beg)
117 (defvar font-lock-end)
118 (defvar recentf-exclude)
119
120 (defvar git-commit-need-summary-line)
121
122 (declare-function dabbrev--reset-global-variables "dabbrev" ())
123 (declare-function dabbrev-capf "dabbrev" ())
124 (declare-function magit-commit-diff--args "magit-commit" ())
125 (declare-function magit-diff--modified-defuns "magit-diff" ())
126 (declare-function magit-diff-arguments "magit-diff" (&optional mode))
127
128 (define-obsolete-variable-alias
129 'git-commit-known-pseudo-headers
130 'git-commit-trailers
131 "git-commit 4.0.0")
132
133 ;;; Options
134 ;;;; Variables
135
136 (defgroup git-commit nil
137 "Edit Git commit messages."
138 :prefix "git-commit-"
139 :link '(info-link "(magit)Editing Commit Messages")
140 :group 'tools)
141
142 (defcustom git-commit-major-mode #'text-mode
143 "Major mode used to edit Git commit messages.
144
145 The major mode configured here is turned on by the minor mode
146 `git-commit-mode'."
147 :group 'git-commit
148 :type '(radio (function-item text-mode)
149 (function-item markdown-mode)
150 (function-item org-mode)
151 (function-item fundamental-mode)
152 (function-item log-edit-mode)
153 (function-item git-commit-elisp-text-mode)
154 (function :tag "Another mode")
155 (const :tag "No major mode")))
156 ;;;###autoload(put 'git-commit-major-mode 'safe-local-variable
157 ;;;###autoload (lambda (val)
158 ;;;###autoload (memq val '(text-mode
159 ;;;###autoload markdown-mode
160 ;;;###autoload org-mode
161 ;;;###autoload fundamental-mode
162 ;;;###autoload log-edit-mode
163 ;;;###autoload git-commit-elisp-text-mode))))
164
165 (defvaralias 'git-commit-mode-hook 'git-commit-setup-hook
166 "This variable is an alias for `git-commit-setup-hook' (which see).
167 Also note that `git-commit-mode' (which see) is not a major-mode.")
168
169 (defcustom git-commit-setup-hook
170 (list #'git-commit-ensure-comment-gap
171 #'git-commit-save-message
172 #'git-commit-setup-capf
173 #'git-commit-setup-changelog-support
174 #'git-commit-setup-auto-fill
175 #'git-commit-propertize-diff
176 #'git-commit-collapse-diff
177 #'git-commit-setup-bug-reference)
178 "Hook run at the end of `git-commit-setup'."
179 :group 'git-commit
180 :type 'hook
181 :get #'magit-hook-custom-get
182 :options '(git-commit-ensure-comment-gap
183 git-commit-save-message
184 git-commit-setup-capf
185 git-commit-setup-changelog-support
186 magit-generate-changelog
187 git-commit-setup-auto-fill
188 git-commit-setup-orglink
189 git-commit-setup-flyspell
190 git-commit-propertize-diff
191 git-commit-collapse-diff
192 git-commit-setup-bug-reference))
193
194 (defcustom git-commit-finish-query-functions
195 (list #'git-commit-check-style-conventions)
196 "List of functions called to query before performing commit.
197
198 The commit message buffer is current while the functions are
199 called. If any of them returns nil, then the commit is not
200 performed and the buffer is not killed. The user should then
201 fix the issue and try again.
202
203 The functions are called with one argument. If it is non-nil,
204 then that indicates that the user used a prefix argument to
205 force finishing the session despite issues. Functions should
206 usually honor this wish and return non-nil."
207 :options '(git-commit-check-style-conventions)
208 :type 'hook
209 :group 'git-commit)
210
211 (defcustom git-commit-style-convention-checks '(non-empty-second-line)
212 "List of checks performed by `git-commit-check-style-conventions'.
213
214 Valid members are `non-empty-second-line' and `overlong-summary-line'.
215 That function is a member of `git-commit-finish-query-functions'."
216 :options '(non-empty-second-line overlong-summary-line)
217 :type '(list :convert-widget custom-hook-convert-widget)
218 :group 'git-commit)
219
220 (defcustom git-commit-summary-max-length 68
221 "Column beyond which characters in the summary lines are highlighted.
222
223 The highlighting indicates that the summary is getting too long
224 by some standards. It does in no way imply that going over the
225 limit a few characters or in some cases even many characters is
226 anything that deserves shaming. It's just a friendly reminder
227 that if you can make the summary shorter, then you might want
228 to consider doing so."
229 :group 'git-commit
230 :safe 'numberp
231 :type 'number)
232
233 (defcustom git-commit-trailers
234 '("Acked-by"
235 "Modified-by"
236 "Reviewed-by"
237 "Signed-off-by"
238 "Tested-by"
239 "Cc"
240 "Reported-by"
241 "Suggested-by"
242 "Co-authored-by"
243 "Co-developed-by")
244 "A list of Git trailers to be highlighted.
245
246 See also manpage git-interpret-trailer(1). This package does
247 not use that Git command, but the initial description still
248 serves as a good introduction."
249 :group 'git-commit
250 :safe (##and (listp %) (all #'stringp %))
251 :type '(repeat string))
252
253 (defcustom git-commit-use-local-message-ring nil
254 "Whether to use a local message ring instead of the global one.
255
256 This can be set globally, in which case every repository gets its
257 own commit message ring, or locally for a single repository."
258 :group 'git-commit
259 :safe 'booleanp
260 :type 'boolean)
261
262 (defcustom git-commit-cd-to-toplevel nil
263 "Whether to set `default-directory' to the worktree in message buffer.
264
265 Editing a commit message is done by visiting a file located in the git
266 directory, usually \"COMMIT_EDITMSG\". As is done when visiting any
267 file, the local value of `default-directory' is set to the directory
268 that contains the file.
269
270 If this option is non-nil, then the local `default-directory' is changed
271 to the working tree from which the commit command was invoked. You may
272 wish to do that, to make it easier to open a file that is located in the
273 working tree, directly from the commit message buffer.
274
275 If the git variable `safe.bareRepository' is set to \"explicit\", then
276 you have to enable this, to be able to commit at all. See issue #5100.
277
278 This option only has an effect if the commit was initiated from Magit."
279 :group 'git-commit
280 :type 'boolean)
281
282 ;;;; Faces
283
284 (defgroup git-commit-faces nil
285 "Faces used for highlighting Git commit messages."
286 :prefix "git-commit-"
287 :group 'git-commit
288 :group 'faces)
289
290 (defface git-commit-summary
291 '((t :inherit font-lock-type-face))
292 "Face used for the summary in commit messages."
293 :group 'git-commit-faces)
294
295 (defface git-commit-overlong-summary
296 '((t :inherit font-lock-warning-face))
297 "Face used for the tail of overlong commit message summaries."
298 :group 'git-commit-faces)
299
300 (defface git-commit-nonempty-second-line
301 '((t :inherit font-lock-warning-face))
302 "Face used for non-whitespace on the second line of commit messages."
303 :group 'git-commit-faces)
304
305 (defface git-commit-keyword
306 '((t :inherit font-lock-string-face))
307 "Face used for keywords in commit messages.
308 In this context a \"keyword\" is text surrounded by brackets."
309 :group 'git-commit-faces)
310
311 (defface git-commit-trailer-token
312 '((t :inherit font-lock-keyword-face))
313 "Face used for Git trailer tokens in commit messages."
314 :group 'git-commit-faces)
315
316 (defface git-commit-trailer-value
317 '((t :inherit font-lock-string-face))
318 "Face used for Git trailer values in commit messages."
319 :group 'git-commit-faces)
320
321 (defface git-commit-comment-branch-local
322 '((t :inherit magit-branch-local))
323 "Face used for names of local branches in commit message comments."
324 :group 'git-commit-faces)
325
326 (defface git-commit-comment-branch-remote
327 '((t :inherit magit-branch-remote))
328 "Face used for names of remote branches in commit message comments."
329 :group 'git-commit-faces)
330
331 (defface git-commit-comment-detached
332 '((t :inherit git-commit-comment-branch-local))
333 "Face used for detached `HEAD' in commit message comments."
334 :group 'git-commit-faces)
335
336 (defface git-commit-comment-heading
337 '((t :inherit git-commit-trailer-token))
338 "Face used for headings in commit message comments."
339 :group 'git-commit-faces)
340
341 (defface git-commit-comment-button
342 '((t :inherit git-commit-comment-heading :underline t))
343 "Face used for buttons in commit message comments."
344 :group 'git-commit-faces)
345
346 (defface git-commit-comment-file
347 '((t :inherit git-commit-trailer-value))
348 "Face used for file names in commit message comments."
349 :group 'git-commit-faces)
350
351 (defface git-commit-comment-action
352 '((t :inherit bold))
353 "Face used for actions in commit message comments."
354 :group 'git-commit-faces)
355
356 ;;; Keymap
357
358 (defvar-keymap git-commit-redundant-bindings
359 :doc "Bindings made redundant by `git-commit-insert-trailer'.
360 This keymap is used as the parent of `git-commit-mode-map',
361 to avoid upsetting muscle-memory. If you would rather avoid
362 the redundant bindings, then set this to nil, before loading
363 `git-commit'."
364 "C-c C-a" #'git-commit-ack
365 "C-c M-i" #'git-commit-suggested
366 "C-c C-m" #'git-commit-modified
367 "C-c C-o" #'git-commit-cc
368 "C-c C-p" #'git-commit-reported
369 "C-c C-r" #'git-commit-review
370 "C-c C-s" #'git-commit-signoff
371 "C-c C-t" #'git-commit-test)
372
373 (defvar-keymap git-commit-mode-map
374 :doc "Keymap used by `git-commit-mode'."
375 :parent git-commit-redundant-bindings
376 "M-p" #'git-commit-prev-message
377 "M-n" #'git-commit-next-message
378 "C-c M-p" #'git-commit-search-message-backward
379 "C-c M-n" #'git-commit-search-message-forward
380 "C-c C-i" #'git-commit-insert-trailer
381 "C-c M-s" #'git-commit-save-message
382 "C-c C-d" 'magit-diff-while-committing
383 "C-c C-w" 'magit-pop-revision-stack)
384
385 ;;; Menu
386
387 (require 'easymenu)
388 (easy-menu-define git-commit-mode-menu git-commit-mode-map
389 "Git Commit Mode Menu."
390 '("Commit"
391 ["Previous" git-commit-prev-message t]
392 ["Next" git-commit-next-message t]
393 "-"
394 ["Ack" git-commit-ack t
395 :help "Insert an 'Acked-by' trailer"]
396 ["Modified-by" git-commit-modified t
397 :help "Insert a 'Modified-by' trailer"]
398 ["Reviewed-by" git-commit-review t
399 :help "Insert a 'Reviewed-by' trailer"]
400 ["Sign-Off" git-commit-signoff t
401 :help "Insert a 'Signed-off-by' trailer"]
402 ["Tested-by" git-commit-test t
403 :help "Insert a 'Tested-by' trailer"]
404 "-"
405 ["CC" git-commit-cc t
406 :help "Insert a 'Cc' trailer"]
407 ["Reported" git-commit-reported t
408 :help "Insert a 'Reported-by' trailer"]
409 ["Suggested" git-commit-suggested t
410 :help "Insert a 'Suggested-by' trailer"]
411 ["Co-authored-by" git-commit-co-authored t
412 :help "Insert a 'Co-authored-by' trailer"]
413 ["Co-developed-by" git-commit-co-developed t
414 :help "Insert a 'Co-developed-by' trailer"]
415 "-"
416 ["Save" git-commit-save-message t]
417 ["Cancel" with-editor-cancel t]
418 ["Commit" with-editor-finish t]))
419
420 ;;; Global Mode
421
422 (define-minor-mode global-git-commit-mode
423 "Edit Git commit messages.
424
425 This global mode arranges for `git-commit-setup' to be called
426 when a Git commit message file is opened. That usually happens
427 when Git uses the Emacsclient as $GIT_EDITOR to have the user
428 provide such a commit message.
429
430 Loading the library `git-commit' by default enables this mode,
431 but the library is not automatically loaded because doing that
432 would pull in many dependencies and increase startup time too
433 much. You can either rely on `magit' loading this library or
434 you can load it explicitly. Autoloading is not an alternative
435 because in this case autoloading would immediately trigger
436 full loading."
437 :group 'git-commit
438 :type 'boolean
439 :global t
440 :init-value t
441 :initialize
442 (lambda (symbol exp)
443 (custom-initialize-default symbol exp)
444 (when global-git-commit-mode
445 (add-hook 'find-file-hook #'git-commit-setup-check-buffer)
446 (remove-hook 'after-change-major-mode-hook
447 #'git-commit-setup-font-lock-in-buffer)))
448 (cond
449 (global-git-commit-mode
450 (add-hook 'find-file-hook #'git-commit-setup-check-buffer)
451 (add-hook 'after-change-major-mode-hook
452 #'git-commit-setup-font-lock-in-buffer))
453 (t
454 (remove-hook 'find-file-hook #'git-commit-setup-check-buffer)
455 (remove-hook 'after-change-major-mode-hook
456 #'git-commit-setup-font-lock-in-buffer))))
457
458 (defconst git-commit-filename-regexp "/\\(\
459 \\(\\(COMMIT\\|NOTES\\|PULLREQ\\|MERGEREQ\\|TAG\\)_EDIT\\|MERGE_\\|\\)MSG\
460 \\|\\(BRANCH\\|EDIT\\)_DESCRIPTION\\)\\'")
461
462 (with-eval-after-load 'recentf
463 (add-to-list 'recentf-exclude git-commit-filename-regexp))
464
465 (add-to-list 'with-editor-file-name-history-exclude git-commit-filename-regexp)
466
467 (defun git-commit-setup-font-lock-in-buffer ()
468 (when (and buffer-file-name
469 (string-match-p git-commit-filename-regexp buffer-file-name))
470 (git-commit-setup-font-lock)))
471
472 (defun git-commit-setup-check-buffer ()
473 (when (and buffer-file-name
474 (string-match-p git-commit-filename-regexp buffer-file-name))
475 (git-commit-setup)))
476
477 (defun git-commit-file-not-found ()
478 ;; cygwin git will pass a cygwin path (/cygdrive/c/foo/.git/...),
479 ;; try to handle this in window-nt Emacs.
480 (when-let*
481 ((file (and (or (string-match-p git-commit-filename-regexp
482 buffer-file-name)
483 (and (boundp 'git-rebase-filename-regexp)
484 (string-match-p git-rebase-filename-regexp
485 buffer-file-name)))
486 (not (file-accessible-directory-p
487 (file-name-directory buffer-file-name)))
488 (magit-expand-git-file-name (substring buffer-file-name 2))))
489 (_(file-accessible-directory-p (file-name-directory file)))
490 (inhibit-read-only t))
491 (insert-file-contents file t)
492 t))
493
494 (when (eq system-type 'windows-nt)
495 (add-hook 'find-file-not-found-functions #'git-commit-file-not-found))
496
497 ;;; Local Mode
498
499 (defvar git-commit-mode)
500
501 (defconst git-commit-default-usage-message "\
502 Type \\[with-editor-finish] to finish, \
503 \\[with-editor-cancel] to cancel, and \
504 \\[git-commit-prev-message] and \\[git-commit-next-message] \
505 to recover older messages")
506
507 (defvar git-commit-usage-message git-commit-default-usage-message
508 "Message displayed when editing a commit message.
509 When this is nil, then `with-editor-usage-message' is displayed
510 instead. One of these messages has to be displayed; otherwise
511 the user gets to see the message displayed by `server-execute'.
512 That message is misleading and because we cannot prevent it from
513 being displayed, we have to immediately show another message to
514 prevent the user from seeing it.")
515
516 (defvar git-commit-header-line-format nil
517 "If non-nil, header line format used by `git-commit-mode'.
518 Used as the local value of `header-line-format', in buffer using
519 `git-commit-mode'. If it is a string, then it is passed through
520 `substitute-command-keys' first. A useful setting may be:
521 (setq git-commit-header-line-format git-commit-default-usage-message)
522 (setq git-commit-usage-message nil) ; show a shorter message")
523
524 (defun git-commit-setup ()
525 ;; If an error occurs when `emacsclient' is used, and it turns out
526 ;; to not be triggered by something in this function, then another
527 ;; likely source are functions on `server-switch-hook'. Debugging
528 ;; is suppressed while running that hook, so it may be necessary to
529 ;; force debugging by modifying those functions directly. Enabling
530 ;; `magit-process-record-invocations' may also help.
531 (let ((gitdir default-directory)
532 (cd (and git-commit-cd-to-toplevel
533 (or (car (rassoc default-directory magit--separated-gitdirs))
534 (magit-toplevel)))))
535 ;; Pretend that git-commit-mode is a major-mode,
536 ;; so that directory-local settings can be used.
537 (let ((default-directory
538 (or (and (not (file-exists-p
539 (expand-file-name ".dir-locals.el" gitdir)))
540 ;; When $GIT_DIR/.dir-locals.el doesn't exist,
541 ;; fallback to $GIT_WORK_TREE/.dir-locals.el,
542 ;; because the maintainer can use the latter
543 ;; to enforce conventions, while s/he has no
544 ;; control over the former.
545 (or cd (magit-toplevel)))
546 gitdir)))
547 (let ((buffer-file-name nil) ; trick hack-dir-local-variables
548 (major-mode 'git-commit-mode)) ; trick dir-locals-collect-variables
549 (hack-dir-local-variables)
550 (hack-local-variables-apply)))
551 (when cd
552 (setq default-directory cd)))
553 (when git-commit-major-mode
554 (let ((auto-mode-alist
555 ;; `set-auto-mode--apply-alist' removes the remote part from
556 ;; the file-name before looking it up in `auto-mode-alist'.
557 ;; For our temporary entry to be found, we have to modify the
558 ;; file-name the same way.
559 (list (cons (concat "\\`"
560 (regexp-quote
561 (or (file-remote-p buffer-file-name 'localname)
562 buffer-file-name))
563 "\\'")
564 git-commit-major-mode)))
565 ;; The major-mode hook might want to consult these minor
566 ;; modes, while the minor-mode hooks might want to consider
567 ;; the major mode.
568 (git-commit-mode t)
569 (with-editor-mode t))
570 (normal-mode t)))
571 ;; Below we instead explicitly show a message.
572 (setq with-editor-show-usage nil)
573 (unless with-editor-mode
574 ;; Maybe already enabled when using `shell-command' or an Emacs shell.
575 (with-editor-mode 1))
576 (add-hook 'with-editor-finish-query-functions
577 #'git-commit-finish-query-functions nil t)
578 (add-hook 'with-editor-pre-finish-hook #'git-commit-save-message nil t)
579 (add-hook 'with-editor-pre-cancel-hook #'git-commit-save-message nil t)
580 (when (fboundp 'magit-commit--reset-command)
581 (add-hook 'with-editor-post-finish-hook #'magit-commit--reset-command)
582 (add-hook 'with-editor-post-cancel-hook #'magit-commit--reset-command))
583 (git-commit-add-post-finish-hook)
584 (setq with-editor-cancel-message
585 #'git-commit-cancel-message)
586 (git-commit-setup-font-lock)
587 (git-commit-prepare-message-ring)
588 (when (boundp 'save-place)
589 (setq save-place nil))
590 (let ((git-commit-mode-hook nil))
591 (git-commit-mode 1))
592 (with-demoted-errors "Error running git-commit-setup-hook: %S"
593 (run-hooks 'git-commit-setup-hook))
594 (set-buffer-modified-p nil)
595 (when$ git-commit-header-line-format
596 (setq header-line-format
597 (if (stringp $) (substitute-command-keys $) $)))
598 (when git-commit-usage-message
599 (setq with-editor-usage-message git-commit-usage-message))
600 (with-editor-usage-message))
601
602 (define-minor-mode git-commit-mode
603 "Auxiliary minor mode used when editing Git commit messages.
604 This mode is only responsible for setting up some key bindings.
605 Don't use it directly; instead enable `global-git-commit-mode'.
606 Variable `git-commit-major-mode' controls which major-mode is
607 used."
608 :lighter "")
609
610 (put 'git-commit-mode 'permanent-local t)
611
612 ;;; Setup
613
614 (defun git-commit-ensure-comment-gap ()
615 "Separate initial empty line from initial comment.
616 If the buffer begins with an empty line followed by a comment, insert
617 an additional newline in between, so that once the users start typing,
618 the input isn't tacked to the comment."
619 (save-excursion
620 (goto-char (point-min))
621 (when (looking-at (format "\\`\n%s" comment-start))
622 (open-line 1))))
623
624 (defun git-commit-setup-capf ()
625 "Teach `complete-symbol' about `dabbrev-capf'.
626 When \"git commit\"'s \"--verbose\" argument is used, this allows
627 completing modified symbols and other text appearing in the diff."
628 (require 'dabbrev)
629 (unless dabbrev--abbrev-char-regexp
630 ;; Initialize (not "reset") variables. See #5545.
631 (dabbrev--reset-global-variables))
632 (add-hook 'completion-at-point-functions #'dabbrev-capf 90 t))
633
634 (defun git-commit-setup-changelog-support ()
635 "Treat ChangeLog entries as unindented paragraphs."
636 (setq-local fill-paragraph-function #'log-edit-fill-entry)
637 (setq-local fill-indent-according-to-mode t)
638 (setq-local paragraph-start (concat paragraph-start "\\|\\*\\|(")))
639
640 (defun git-commit-setup-auto-fill ()
641 "Unconditionally turn on Auto Fill mode.
642 Ensure auto filling happens everywhere, except in the summary line."
643 (auto-fill-mode 1)
644 (setq-local comment-auto-fill-only-comments nil)
645 (when git-commit-need-summary-line
646 (setq-local auto-fill-function #'git-commit--auto-fill-except-summary)))
647
648 (defun git-commit--auto-fill-except-summary ()
649 "Do not fill summary line."
650 (unless (eq (line-beginning-position) 1)
651 (do-auto-fill)))
652
653 (defun git-commit-setup-orglink ()
654 "Turn on Orglink mode if it is available.
655 If `git-commit-major-mode' is `org-mode', then silently forgo
656 turning on `orglink-mode'."
657 (when (and (not (derived-mode-p 'org-mode))
658 (boundp 'orglink-match-anywhere)
659 (fboundp 'orglink-mode))
660 (setq-local orglink-match-anywhere t)
661 (orglink-mode 1)))
662
663 (defun git-commit-setup-flyspell ()
664 "Unconditionally turn on Flyspell mode.
665 Also check text that is already in the buffer, while avoiding to check
666 most text that Git will strip from the final message, such as the last
667 comment and anything below the cut line (\"--- >8 ---\")."
668 (require 'flyspell)
669 (flyspell-mode 1)
670 (setq flyspell-generic-check-word-predicate #'git-commit--flyspell-verify)
671 (let ((end nil)
672 ;; The "cut line" is defined in "git/wt-status.c". It appears
673 ;; in the commit message when `commit.verbose' is set to true.
674 (cut-line-regex (format "^%s -\\{8,\\} >8 -\\{8,\\}$" comment-start))
675 (comment-start-regex (format "^\\(%s\\|$\\)" comment-start)))
676 (save-excursion
677 (goto-char (or (re-search-forward cut-line-regex nil t)
678 (point-max)))
679 (while (and (not (bobp)) (looking-at comment-start-regex))
680 (forward-line -1))
681 (unless (looking-at comment-start-regex)
682 (forward-line))
683 (setq end (point)))
684 (flyspell-region (point-min) end)))
685
686 (defun git-commit--flyspell-verify ()
687 "Do not check spelling in comments."
688 (not (= (char-after (line-beginning-position))
689 (aref comment-start 0))))
690
691 (defun git-commit-collapse-diff ()
692 "Collapse inline diff and add button to allow expanding it."
693 (save-excursion
694 (goto-char (point-min))
695 (when (re-search-forward (format "%s -+ >8 -+" comment-start) nil t)
696 (let ((elt '(git-commit-diff t)))
697 (add-to-invisibility-spec elt)
698 (make-button (line-beginning-position) (point)
699 'face 'git-commit-comment-button
700 'keymap (define-keymap :parent button-map
701 "<return>" #'push-button
702 "<tab>" #'push-button)
703 'action (lambda (_)
704 (if (memq elt buffer-invisibility-spec)
705 (remove-from-invisibility-spec elt)
706 (add-to-invisibility-spec elt))
707 ;; KLUDGE Force "redisplay".
708 (when-let ((w1 (selected-window))
709 (w2 (next-window)))
710 (select-window w2)
711 (select-window w1)))))
712 (let ((ov (make-overlay (point) (point-max))))
713 (overlay-put ov 'invisible 'git-commit-diff)))))
714
715 (defun git-commit-setup-bug-reference ()
716 "Turn on `bug-reference-mode' while working around a limitation of VC.
717 `bug-reference-try-setup-from-vc' doesn't work when visiting a file in
718 the Git directory."
719 (magit-with-toplevel
720 (let ((buffer-file-name nil))
721 (bug-reference-mode 1))))
722
723 ;;; Finish
724
725 (defun git-commit-finish-query-functions (force)
726 (run-hook-with-args-until-failure
727 'git-commit-finish-query-functions force))
728
729 (defun git-commit-check-style-conventions (force)
730 "Check for violations of certain basic style conventions.
731
732 For each violation ask the user if she wants to proceed anyway.
733 Option `git-commit-style-convention-checks' controls which
734 conventions are checked."
735 (or force
736 (save-excursion
737 (goto-char (point-min))
738 (re-search-forward (git-commit-summary-regexp) nil t)
739 (if (equal (match-str 1) "")
740 t ; Just try; we don't know whether --allow-empty-message was used.
741 (and (or (not (memq 'overlong-summary-line
742 git-commit-style-convention-checks))
743 (equal (match-str 2) "")
744 (y-or-n-p "Summary line is too long. Commit anyway? "))
745 (or (not (memq 'non-empty-second-line
746 git-commit-style-convention-checks))
747 (not (match-str 3))
748 (y-or-n-p "Second line is not empty. Commit anyway? ")))))))
749
750 (defun git-commit-cancel-message ()
751 (message
752 (concat "Commit canceled"
753 (and (memq 'git-commit-save-message with-editor-pre-cancel-hook)
754 ". Message saved to `log-edit-comment-ring'"))))
755
756 ;;; History
757
758 (defun git-commit-prev-message (arg)
759 "Cycle backward through message history, after saving current message.
760 With a numeric prefix ARG, go back ARG messages."
761 (interactive "*p")
762 (let ((len (ring-length log-edit-comment-ring)))
763 (if (<= len 0)
764 (progn (message "Empty comment ring") (ding))
765 ;; Unlike `log-edit-previous-comment' we save the current
766 ;; non-empty and newly written comment, because otherwise
767 ;; it would be irreversibly lost.
768 (when-let* ((message (git-commit-buffer-message))
769 (_(not (ring-member log-edit-comment-ring message))))
770 (ring-insert log-edit-comment-ring message)
771 (incf arg)
772 (setq len (ring-length log-edit-comment-ring)))
773 ;; Delete the message but not the instructions at the end.
774 (save-restriction
775 (goto-char (point-min))
776 (narrow-to-region
777 (point)
778 (if (re-search-forward (concat "^" comment-start) nil t)
779 (max 1 (- (point) 2))
780 (point-max)))
781 (delete-region (point-min) (point)))
782 (setq log-edit-comment-ring-index (log-edit-new-comment-index arg len))
783 (message "Comment %d" (1+ log-edit-comment-ring-index))
784 (insert (ring-ref log-edit-comment-ring log-edit-comment-ring-index)))))
785
786 (defun git-commit-next-message (arg)
787 "Cycle forward through message history, after saving current message.
788 With a numeric prefix ARG, go forward ARG messages."
789 (interactive "*p")
790 (git-commit-prev-message (- arg)))
791
792 (defun git-commit-search-message-backward (string)
793 "Search backward through message history for a match for STRING.
794 Save current message first."
795 (interactive
796 (list (read-string (format-prompt "Comment substring"
797 log-edit-last-comment-match)
798 nil nil log-edit-last-comment-match)))
799 (cl-letf (((symbol-function #'log-edit-previous-comment)
800 (symbol-function #'git-commit-prev-message)))
801 (log-edit-comment-search-backward string)))
802
803 (defun git-commit-search-message-forward (string)
804 "Search forward through message history for a match for STRING.
805 Save current message first."
806 (interactive
807 (list (read-string (format-prompt "Comment substring"
808 log-edit-last-comment-match)
809 nil nil log-edit-last-comment-match)))
810 (cl-letf (((symbol-function #'log-edit-previous-comment)
811 (symbol-function #'git-commit-prev-message)))
812 (log-edit-comment-search-forward string)))
813
814 (defun git-commit-save-message ()
815 "Save current message to `log-edit-comment-ring'."
816 (interactive)
817 (cond-let
818 ([message (git-commit-buffer-message)]
819 (when-let ((index (ring-member log-edit-comment-ring message)))
820 (ring-remove log-edit-comment-ring index))
821 (ring-insert log-edit-comment-ring message)
822 (when git-commit-use-local-message-ring
823 (magit-repository-local-set 'log-edit-comment-ring
824 log-edit-comment-ring))
825 (message "Message saved"))
826 ((message "Only whitespace and/or comments; message not saved"))))
827
828 (defun git-commit-prepare-message-ring ()
829 (make-local-variable 'log-edit-comment-ring-index)
830 (when git-commit-use-local-message-ring
831 (setq-local log-edit-comment-ring
832 (magit-repository-local-get
833 'log-edit-comment-ring
834 (make-ring log-edit-maximum-comment-ring-size)))))
835
836 (defun git-commit-buffer-message ()
837 (let ((flush (concat "^" comment-start))
838 (str (buffer-substring-no-properties (point-min) (point-max))))
839 (with-temp-buffer
840 (insert str)
841 (goto-char (point-min))
842 (when (re-search-forward (concat flush " -+ >8 -+$") nil t)
843 (delete-region (line-beginning-position) (point-max)))
844 (goto-char (point-min))
845 (flush-lines flush)
846 (goto-char (point-max))
847 (unless (eq (char-before) ?\n)
848 (insert ?\n))
849 (setq str (buffer-string)))
850 (and (not (string-match "\\`[ \t\n\r]*\\'" str))
851 (progn
852 (when (string-match "\\`\n\\{2,\\}" str)
853 (setq str (replace-match "\n" t t str)))
854 (when (string-match "\n\\{2,\\}\\'" str)
855 (setq str (replace-match "\n" t t str)))
856 str))))
857
858 ;;; Changelog
859
860 (defun git-commit--modified-defuns ()
861 (if (save-excursion
862 (goto-char (point-min))
863 (re-search-forward "^diff --git" nil t))
864 (magit-diff--modified-defuns)
865 (with-temp-buffer
866 (pcase-let ((`(,rev ,arg) (magit-commit-diff--args)))
867 (save-excursion
868 (magit-git-insert "diff" "-p" arg (car (magit-diff-arguments)) rev)))
869 (magit-diff--modified-defuns))))
870
871 ;;;###autoload
872 (defun git-commit-insert-changelog-gnu ()
873 "Insert a GNU-style changelog at point while authorig a commit message.
874
875 The modified definitions are extracted from the diff in the message
876 buffer, which is only available if \"git commit\" was invoked with
877 \"--verbose\"."
878 (interactive)
879 (unless git-commit-mode
880 (user-error "Not in a commit message buffer"))
881 ;; Like `change-log-insert-entries'.
882 (pcase-dolist (`(,file . ,defuns) (git-commit--modified-defuns))
883 (if (not defuns)
884 (insert "* " file ":\n")
885 (insert "* " file " ")
886 (dolist (def defuns)
887 (insert "(" def "):\n")))))
888
889 ;;;###autoload
890 (defun git-commit-insert-changelog-plain ()
891 "Insert a simple changelog at point while authorig a commit message.
892
893 Defuns are slightly indented and quoted like in elisp docstrings.
894 The exact format is still subject to change.
895
896 The modified definitions are extracted from the diff in the message
897 buffer, which is only available if \"git commit\" was invoked with
898 \"--verbose\"."
899 (interactive)
900 (unless git-commit-mode
901 (user-error "Not in a commit message buffer"))
902 (pcase-dolist (`(,file . ,defuns) (git-commit--modified-defuns))
903 (insert file ":\n")
904 (dolist (def defuns)
905 (insert " `" def "'\n"))))
906
907 ;;; Trailers
908
909 (transient-define-prefix git-commit-insert-trailer ()
910 "Insert a commit message trailer.
911
912 See also manpage git-interpret-trailer(1). This command does
913 not use that Git command, but the initial description still
914 serves as a good introduction."
915 [[:description (##cond (prefix-arg
916 "Insert trailer ... by someone ")
917 ("Insert trailer ... by yourself"))
918 ("a" "Ack" git-commit-ack)
919 ("m" "Modified" git-commit-modified)
920 ("r" "Reviewed" git-commit-review)
921 ("s" "Signed-off" git-commit-signoff)
922 ("t" "Tested" git-commit-test)]
923 ["Insert trailer ... by someone"
924 ("C-c" "Cc" git-commit-cc)
925 ("C-r" "Reported" git-commit-reported)
926 ("C-i" "Suggested" git-commit-suggested)
927 ("C-a" "Co-authored" git-commit-co-authored)
928 ("C-d" "Co-developed" git-commit-co-developed)]]
929 ["Insert changelog"
930 ("l g" "GNU-style" git-commit-insert-changelog-gnu)
931 ("l p" "plain" git-commit-insert-changelog-plain)])
932
933 (defun git-commit-ack (name mail)
934 "Insert a trailer acknowledging that you have looked at the commit."
935 (interactive (git-commit-get-ident "Acked-by"))
936 (git-commit--insert-ident-trailer "Acked-by" name mail))
937
938 (defun git-commit-modified (name mail)
939 "Insert a trailer to signal that you have modified the commit."
940 (interactive (git-commit-get-ident "Modified-by"))
941 (git-commit--insert-ident-trailer "Modified-by" name mail))
942
943 (defun git-commit-review (name mail)
944 "Insert a trailer acknowledging that you have reviewed the commit.
945 With a prefix argument, prompt for another person who performed a
946 review."
947 (interactive (git-commit-get-ident "Reviewed-by"))
948 (git-commit--insert-ident-trailer "Reviewed-by" name mail))
949
950 (defun git-commit-signoff (name mail)
951 "Insert a trailer to sign off the commit.
952 With a prefix argument, prompt for another person who signed off."
953 (interactive (git-commit-get-ident "Signed-off-by"))
954 (git-commit--insert-ident-trailer "Signed-off-by" name mail))
955
956 (defun git-commit-test (name mail)
957 "Insert a trailer acknowledging that you have tested the commit.
958 With a prefix argument, prompt for another person who tested."
959 (interactive (git-commit-get-ident "Tested-by"))
960 (git-commit--insert-ident-trailer "Tested-by" name mail))
961
962 (defun git-commit-cc (name mail)
963 "Insert a trailer mentioning someone who might be interested."
964 (interactive (git-commit-read-ident "Cc"))
965 (git-commit--insert-ident-trailer "Cc" name mail))
966
967 (defun git-commit-reported (name mail)
968 "Insert a trailer mentioning the person who reported the issue."
969 (interactive (git-commit-read-ident "Reported-by"))
970 (git-commit--insert-ident-trailer "Reported-by" name mail))
971
972 (defun git-commit-suggested (name mail)
973 "Insert a trailer mentioning the person who suggested the change."
974 (interactive (git-commit-read-ident "Suggested-by"))
975 (git-commit--insert-ident-trailer "Suggested-by" name mail))
976
977 (defun git-commit-co-authored (name mail)
978 "Insert a trailer mentioning the person who co-authored the commit."
979 (interactive (git-commit-read-ident "Co-authored-by"))
980 (git-commit--insert-ident-trailer "Co-authored-by" name mail))
981
982 (defun git-commit-co-developed (name mail)
983 "Insert a trailer mentioning the person who co-developed the commit."
984 (interactive (git-commit-read-ident "Co-developed-by"))
985 (git-commit--insert-ident-trailer "Co-developed-by" name mail))
986
987 (defun git-commit-get-ident (&optional prompt)
988 "Return name and email of the user or read another name and email.
989 If PROMPT and `current-prefix-arg' are both non-nil, read name
990 and email using `git-commit-read-ident' (which see), otherwise
991 return name and email of the current user (you)."
992 (if (and prompt current-prefix-arg)
993 (git-commit-read-ident prompt)
994 (list (or (getenv "GIT_AUTHOR_NAME")
995 (getenv "GIT_COMMITTER_NAME")
996 (with-demoted-errors "Error running 'git config user.name': %S"
997 (magit-get "user.name"))
998 user-full-name
999 (read-string "Name: "))
1000 (or (getenv "GIT_AUTHOR_EMAIL")
1001 (getenv "GIT_COMMITTER_EMAIL")
1002 (getenv "EMAIL")
1003 (with-demoted-errors "Error running 'git config user.email': %S"
1004 (magit-get "user.email"))
1005 (read-string "Email: ")))))
1006
1007 (defalias 'git-commit-self-ident #'git-commit-get-ident)
1008
1009 (defvar git-commit-read-ident-history nil)
1010
1011 (defun git-commit-read-ident (prompt)
1012 "Read a name and email, prompting with PROMPT, and return them.
1013 Read them using a single prompt, offering past commit authors as
1014 completion candidates. The input must have the form \"NAME <EMAIL>\"."
1015 (let ((str (magit-completing-read
1016 prompt
1017 (sort (delete-dups
1018 (magit-git-lines "log" "-n9999" "--format=%aN <%ae>"))
1019 #'string<)
1020 nil 'any nil 'git-commit-read-ident-history)))
1021 (save-match-data
1022 (if (string-match "\\`\\([^<]+\\) *<\\([^>]+\\)>\\'" str)
1023 (list (save-match-data (string-trim (match-str 1 str)))
1024 (string-trim (match-str 2 str)))
1025 (user-error "Invalid input")))))
1026
1027 (defun git-commit--insert-ident-trailer (trailer name email)
1028 (git-commit--insert-trailer trailer (format "%s <%s>" name email)))
1029
1030 (defun git-commit--insert-trailer (trailer value)
1031 (git-commit--insert-trailer-1 (format "%s: %s\n" trailer value)))
1032
1033 (defun git-commit--insert-trailer-1 (string &optional before-trailers)
1034 (save-excursion
1035 (goto-char (point-min))
1036 (while (looking-at comment-start)
1037 (forward-line))
1038 (when (or (eobp) (looking-at "diff --git"))
1039 (goto-char (point-min))
1040 (save-excursion (insert ?\n)))
1041 (let ((bound (and (not (or (bobp) (eobp))) (point))))
1042 (goto-char (point-max))
1043 (unless (or (bobp) (= (char-before) ?\n))
1044 (insert ?\n))
1045 (cond (before-trailers
1046 (git-commit--goto-insert-position bound)
1047 (while (re-search-backward (git-commit--trailer-regexp) nil t))
1048 (unless (looking-back "\n\n" nil)
1049 (insert ?\n)))
1050 ((re-search-backward (git-commit--trailer-regexp) nil t)
1051 (goto-char (match-end 0))
1052 (if (eobp) (insert ?\n) (forward-char)))
1053 (t
1054 (git-commit--goto-insert-position bound)
1055 (unless (looking-back "\n\n" nil)
1056 (insert ?\n)))))
1057 (insert string)
1058 (unless (ignore-errors (= (char-before) ?\n)) (insert ?\n))
1059 (unless (ignore-errors (= (char-after) ?\n)) (insert ?\n))))
1060
1061 (defun git-commit--goto-insert-position (bound)
1062 (let ((match (point)))
1063 (cond ((re-search-backward (format "^%s -+ >8 -+" comment-start) nil t))
1064 ((and (eobp) (bolp))
1065 (forward-line -1)))
1066 (while (and (or (looking-at comment-start)
1067 (looking-at "[\s\t]*$"))
1068 (or (not bound) (> (point) bound))
1069 (not (bobp)))
1070 (setq match (point))
1071 (forward-line -1))
1072 (goto-char match)))
1073
1074 ;;; Font-Lock
1075
1076 (defvar-local git-commit-need-summary-line t
1077 "Whether the text should have a heading that is separated from the body.
1078
1079 For commit messages that is a convention that should not
1080 be violated. For notes it is up to the user. If you do
1081 not want to insist on an empty second line here, then use
1082 something like:
1083
1084 (add-hook \\='git-commit-setup-hook
1085 (lambda ()
1086 (when (equal (file-name-nondirectory (buffer-file-name))
1087 \"NOTES_EDITMSG\")
1088 (setq git-commit-need-summary-line nil))))")
1089
1090 (defun git-commit--trailer-regexp ()
1091 (format
1092 "^\\(?:\\(%s:\\)\\( .*\\)\\|\\([-a-zA-Z]+\\): \\([^<\n]+? <[^>\n]+>\\)\\)"
1093 (regexp-opt git-commit-trailers)))
1094
1095 (defun git-commit-summary-regexp ()
1096 (if git-commit-need-summary-line
1097 (concat
1098 ;; Leading empty lines and comments
1099 (format "\\`\\(?:^\\(?:\\s-*\\|%s.*\\)\n\\)*" comment-start)
1100 ;; Summary line
1101 (format "\\(.\\{0,%d\\}\\)\\(.*\\)" git-commit-summary-max-length)
1102 ;; Non-empty non-comment second line
1103 (format "\\(?:\n%s\\|\n\\(.+\\)\\)?" comment-start))
1104 "\\(EASTER\\) \\(EGG\\)"))
1105
1106 (defun git-commit-extend-region-summary-line ()
1107 "Identify the multiline summary-regexp construct.
1108 Added to `font-lock-extend-region-functions'."
1109 (save-excursion
1110 (save-match-data
1111 (goto-char (point-min))
1112 (when (looking-at (git-commit-summary-regexp))
1113 (let ((summary-beg (match-beginning 0))
1114 (summary-end (match-end 0)))
1115 (when (or (< summary-beg font-lock-beg summary-end)
1116 (< summary-beg font-lock-end summary-end))
1117 (setq font-lock-beg (min font-lock-beg summary-beg))
1118 (setq font-lock-end (max font-lock-end summary-end))))))))
1119
1120 (defvar-local git-commit--branch-name-regexp nil)
1121
1122 (defconst git-commit-comment-headings
1123 '("Changes to be committed:"
1124 "Untracked files:"
1125 "Changed but not updated:"
1126 "Changes not staged for commit:"
1127 "Unmerged paths:"
1128 "Author:"
1129 "Date:")
1130 "Also fontified outside of comments in `git-commit-font-lock-keywords-2'.")
1131
1132 (defconst git-commit-font-lock-keywords-1
1133 '(;; Trailers
1134 (eval . `(,(git-commit--trailer-regexp)
1135 (1 'git-commit-trailer-token nil t)
1136 (2 'git-commit-trailer-value nil t)
1137 (3 'git-commit-trailer-token nil t)
1138 (4 'git-commit-trailer-value nil t)))
1139 ;; Summary
1140 (eval . `(,(git-commit-summary-regexp)
1141 (1 'git-commit-summary)))
1142 ;; - Keyword [aka "text in brackets"] (overrides summary)
1143 ("\\[[^][]+?\\]"
1144 (0 'git-commit-keyword t))
1145 ;; - Non-empty second line (overrides summary and note)
1146 (eval . `(,(git-commit-summary-regexp)
1147 (2 'git-commit-overlong-summary t t)
1148 (3 'git-commit-nonempty-second-line t t)))))
1149
1150 (defconst git-commit-font-lock-keywords-2
1151 `(,@git-commit-font-lock-keywords-1
1152 ;; Comments
1153 (eval . `(,(format "^%s.*" comment-start)
1154 (0 'font-lock-comment-face append)))
1155 (eval . `(,(format "^%s On branch \\(.*\\)" comment-start)
1156 (1 'git-commit-comment-branch-local t)))
1157 (eval . `(,(format "^%s \\(HEAD\\) detached at" comment-start)
1158 (1 'git-commit-comment-detached t)))
1159 (eval . `(,(format "^%s %s" comment-start
1160 (regexp-opt git-commit-comment-headings t))
1161 (1 'git-commit-comment-heading t)))
1162 (eval . `(,(format "^%s\t\\(?:\\([^:\n]+\\):\\s-+\\)?\\(.*\\)" comment-start)
1163 (1 'git-commit-comment-action t t)
1164 (2 'git-commit-comment-file t)))
1165 ;; "commit HASH"
1166 (eval . '("^commit [[:alnum:]]+$"
1167 (0 'git-commit-trailer-value)))
1168 ;; `git-commit-comment-headings' (but not in commented lines)
1169 (eval . `(,(format "\\(?:^%s[[:blank:]]+.+$\\)"
1170 (regexp-opt git-commit-comment-headings))
1171 (0 'git-commit-trailer-value)))))
1172
1173 (defconst git-commit-font-lock-keywords-3
1174 `(,@git-commit-font-lock-keywords-2
1175 ;; More comments
1176 (eval
1177 ;; Your branch is ahead of 'master' by 3 commits.
1178 ;; Your branch is behind 'master' by 2 commits, and can be fast-forwarded.
1179 . `(,(format
1180 "^%s Your branch is \\(?:ahead\\|behind\\) of '%s' by \\([0-9]*\\)"
1181 comment-start git-commit--branch-name-regexp)
1182 (1 'git-commit-comment-branch-local t)
1183 (2 'git-commit-comment-branch-remote t)
1184 (3 'bold t)))
1185 (eval
1186 ;; Your branch is up to date with 'master'.
1187 ;; Your branch and 'master' have diverged,
1188 . `(,(format
1189 "^%s Your branch \\(?:is up[- ]to[- ]date with\\|and\\) '%s'"
1190 comment-start git-commit--branch-name-regexp)
1191 (1 'git-commit-comment-branch-local t)
1192 (2 'git-commit-comment-branch-remote t)))
1193 (eval
1194 ;; and have 1 and 2 different commits each, respectively.
1195 . `(,(format
1196 "^%s and have \\([0-9]*\\) and \\([0-9]*\\) commits each"
1197 comment-start)
1198 (1 'bold t)
1199 (2 'bold t)))))
1200
1201 (defvar git-commit-font-lock-keywords git-commit-font-lock-keywords-3
1202 "Font-Lock keywords for Git-Commit mode.")
1203
1204 (defun git-commit-setup-font-lock ()
1205 (with-demoted-errors "Error running git-commit-setup-font-lock: %S"
1206 (let ((table (make-syntax-table (syntax-table))))
1207 (when comment-start
1208 (modify-syntax-entry (string-to-char comment-start) "." table))
1209 (modify-syntax-entry ?# "." table)
1210 (modify-syntax-entry ?\" "." table)
1211 (modify-syntax-entry ?\' "." table)
1212 (modify-syntax-entry ?` "." table)
1213 (set-syntax-table table))
1214 (setq-local comment-start (or (magit-get "core.commentchar") "#"))
1215 (setq-local comment-start-skip (format "^%s+[\s\t]*" comment-start))
1216 (setq-local comment-end "")
1217 (setq-local comment-end-skip "\n")
1218 (setq-local comment-use-syntax nil)
1219 (when (and (derived-mode-p 'markdown-mode)
1220 (fboundp 'markdown-fill-paragraph))
1221 (setq-local fill-paragraph-function
1222 (lambda (&optional justify)
1223 (and (not (= (char-after (line-beginning-position))
1224 (aref comment-start 0)))
1225 (markdown-fill-paragraph justify)))))
1226 (setq-local git-commit--branch-name-regexp
1227 ;; When using cygwin git, we may end up in a
1228 ;; non-existing directory, which would cause
1229 ;; any git calls to signal an error.
1230 (if (file-accessible-directory-p default-directory)
1231 ;; Font-Lock wants every submatch to succeed, so
1232 ;; also match the empty string. Avoid listing
1233 ;; remote branches and using `regexp-quote',
1234 ;; because in repositories that have thousands of
1235 ;; branches that would be very slow. See #4353.
1236 (format "\\(\\(?:%s\\)\\|\\)\\([^']+\\)"
1237 (string-join (magit-list-local-branch-names) "\\|"))
1238 "\\([^']*\\)"))
1239 (setq-local font-lock-multiline t)
1240 (add-hook 'font-lock-extend-region-functions
1241 #'git-commit-extend-region-summary-line
1242 t t)
1243 (font-lock-add-keywords nil git-commit-font-lock-keywords)))
1244
1245 (defun git-commit-propertize-diff ()
1246 (require 'diff-mode)
1247 (save-excursion
1248 (goto-char (point-min))
1249 (when (re-search-forward "^diff --git" nil t)
1250 (beginning-of-line)
1251 (let ((buffer (current-buffer)))
1252 (insert
1253 (with-temp-buffer
1254 (insert
1255 (with-current-buffer buffer
1256 (prog1 (buffer-substring-no-properties (point) (point-max))
1257 (delete-region (point) (point-max)))))
1258 (let ((diff-default-read-only nil))
1259 (diff-mode))
1260 ;; These won't survive copying to another buffer,
1261 ;; so let's not waste any time. See #5483.
1262 (setq-local diff-refine nil)
1263 (setq-local diff-font-lock-syntax nil)
1264 (setq-local diff-font-lock-prettify nil)
1265 (let ((font-lock-verbose nil)
1266 (font-lock-support-mode nil))
1267 (font-lock-ensure))
1268 (let ((pos (point-min)))
1269 (while-let ((next (next-single-property-change pos 'face)))
1270 (put-text-property pos next 'font-lock-face
1271 (get-text-property pos 'face))
1272 (setq pos next))
1273 (put-text-property pos (point-max) 'font-lock-face
1274 (get-text-property pos 'face)))
1275 (buffer-string)))))))
1276
1277 ;;; Elisp Text Mode
1278
1279 (define-derived-mode git-commit-elisp-text-mode text-mode "ElText"
1280 "Major mode for editing commit messages of elisp projects.
1281 This is intended for use as `git-commit-major-mode' for projects
1282 that expect `symbols' to look like this. I.e., like they look in
1283 Elisp doc-strings, including this one. Unlike in doc-strings,
1284 \"strings\" also look different than the other text."
1285 (setq font-lock-defaults '(git-commit-elisp-text-mode-keywords)))
1286
1287 (defvar git-commit-elisp-text-mode-keywords
1288 `((,(concat "[`‘]\\(" lisp-mode-symbol-regexp "\\)['’]")
1289 (1 font-lock-constant-face prepend))
1290 ("\"[^\"]*\"" (0 font-lock-string-face prepend))))
1291
1292 ;;; Post Hook
1293
1294 (defcustom git-commit-post-finish-hook nil
1295 "Hook run after the user finished writing a commit message.
1296
1297 \\<with-editor-mode-map>\
1298 This hook is only run after pressing \\[with-editor-finish] in a buffer used
1299 to edit a commit message. If a commit is created without the
1300 user typing a message into a buffer, then this hook is not run.
1301
1302 This hook is not run until the new commit has been created. If
1303 that takes Git longer than `git-commit-post-finish-hook-timeout'
1304 seconds, then this hook isn't run at all. For certain commands
1305 such as `magit-rebase-continue' this hook is never run because
1306 doing so would lead to a race condition.
1307
1308 Also see `magit-post-commit-hook'."
1309 :group 'git-commit
1310 :type 'hook
1311 :get #'magit-hook-custom-get)
1312
1313 (defcustom git-commit-post-finish-hook-timeout 2
1314 "Time in seconds to wait for git to create a commit.
1315
1316 The hook `git-commit-post-finish-hook' (which see) is run only
1317 after git is done creating a commit. If it takes longer than
1318 `git-commit-post-finish-hook-timeout' seconds to create the
1319 commit, then the hook is not run at all."
1320 :group 'git-commit
1321 :safe 'numberp
1322 :type 'number)
1323
1324 (defun git-commit-add-post-finish-hook ()
1325 (unless (memq last-command
1326 '(magit-sequencer-continue
1327 magit-sequencer-skip
1328 magit-am-continue
1329 magit-am-skip
1330 magit-rebase-continue
1331 magit-rebase-skip))
1332 (add-hook 'with-editor-post-finish-hook
1333 (apply-partially #'git-commit-run-post-finish-hook
1334 (magit-rev-parse "HEAD"))
1335 nil t)))
1336
1337 (defun git-commit-run-post-finish-hook (previous)
1338 (when git-commit-post-finish-hook
1339 (if (with-timeout (git-commit-post-finish-hook-timeout)
1340 (while (equal (magit-rev-parse "HEAD") previous)
1341 (sit-for 0.01))
1342 t)
1343 (run-hooks 'git-commit-post-finish-hook)
1344 (message "No commit created after %s second. Not running %s."
1345 git-commit-post-finish-hook-timeout
1346 'git-commit-post-finish-hook))))
1347
1348 ;;; _
1349
1350 (define-obsolete-function-alias
1351 'git-commit-insert-pseudo-header
1352 'git-commit-insert-trailer
1353 "git-commit 4.0.0")
1354 (define-obsolete-function-alias
1355 'git-commit-insert-header
1356 'git-commit--insert-ident-trailer
1357 "git-commit 4.0.0")
1358 (define-obsolete-face-alias
1359 'git-commit-pseudo-header
1360 'git-commit-trailer-value
1361 "git-commit 4.0.0")
1362 (define-obsolete-face-alias
1363 'git-commit-known-pseudo-header
1364 'git-commit-trailer-token
1365 "git-commit 4.0.0")
1366
1367 (define-obsolete-function-alias
1368 'git-commit-turn-on-auto-fill
1369 'git-commit-setup-auto-fill
1370 "git-commit 4.6.0")
1371 (define-obsolete-function-alias
1372 'git-commit-turn-on-flyspell
1373 'git-commit-setup-flyspell
1374 "git-commit 4.6.0")
1375 (define-obsolete-function-alias
1376 'git-commit-turn-on-orglink
1377 'git-commit-setup-orglink
1378 "git-commit 4.6.0")
1379
1380 (provide 'git-commit)
1381 ;; Local Variables:
1382 ;; read-symbol-shorthands: (
1383 ;; ("and$" . "cond-let--and$")
1384 ;; ("thread$" . "cond-let--thread$")
1385 ;; ("when$" . "cond-let--when$")
1386 ;; ("and-let*" . "cond-let--and-let*")
1387 ;; ("and-let" . "cond-let--and-let")
1388 ;; ("if-let*" . "cond-let--if-let*")
1389 ;; ("if-let" . "cond-let--if-let")
1390 ;; ("when-let*" . "cond-let--when-let*")
1391 ;; ("when-let" . "cond-let--when-let")
1392 ;; ("while-let*" . "cond-let--while-let*")
1393 ;; ("while-let" . "cond-let--while-let")
1394 ;; ("match-string" . "match-string")
1395 ;; ("match-str" . "match-string-no-properties"))
1396 ;; End:
1397 ;;; git-commit.el ends here