dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

git-commit.el (43224B)


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