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