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