| 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 |