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