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 |