config

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

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