config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

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