config

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

magit-extras.el (36681B)


      1 ;;; magit-extras.el --- Additional functionality for Magit  -*- lexical-binding:t -*-
      2 
      3 ;; Copyright (C) 2008-2024 The Magit Project Contributors
      4 
      5 ;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
      6 ;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
      7 
      8 ;; SPDX-License-Identifier: GPL-3.0-or-later
      9 
     10 ;; Magit is free software: you can redistribute it and/or modify it
     11 ;; under the terms of the GNU General Public License as published by
     12 ;; the Free Software Foundation, either version 3 of the License, or
     13 ;; (at your option) any later version.
     14 ;;
     15 ;; Magit is distributed in the hope that it will be useful, but WITHOUT
     16 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     17 ;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
     18 ;; License for more details.
     19 ;;
     20 ;; You should have received a copy of the GNU General Public License
     21 ;; along with Magit.  If not, see <https://www.gnu.org/licenses/>.
     22 
     23 ;;; Commentary:
     24 
     25 ;; Additional functionality for Magit.
     26 
     27 ;;; Code:
     28 
     29 (require 'magit)
     30 
     31 ;; For `magit-do-async-shell-command'.
     32 (declare-function dired-read-shell-command "dired-aux" (prompt arg files))
     33 ;; For `magit-project-status'.
     34 (declare-function vc-git-command "vc-git"
     35                   (buffer okstatus file-or-list &rest flags))
     36 
     37 (defvar ido-exit)
     38 (defvar ido-fallback)
     39 (defvar project-prefix-map)
     40 (defvar project-switch-commands)
     41 
     42 (defgroup magit-extras nil
     43   "Additional functionality for Magit."
     44   :group 'magit-extensions)
     45 
     46 ;;; Git Tools
     47 ;;;; Git-Mergetool
     48 
     49 ;;;###autoload (autoload 'magit-git-mergetool "magit-extras" nil t)
     50 (transient-define-prefix magit-git-mergetool (file args &optional transient)
     51   "Resolve conflicts in FILE using \"git mergetool --gui\".
     52 With a prefix argument allow changing ARGS using a transient
     53 popup.  See info node `(magit) Ediffing' for information about
     54 alternative commands."
     55   :man-page "git-mergetool"
     56   ["Settings"
     57    ("-t" magit-git-mergetool:--tool)
     58    ("=t" magit-merge.guitool)
     59    ("=T" magit-merge.tool)
     60    ("-r" magit-mergetool.hideResolved)
     61    ("-b" magit-mergetool.keepBackup)
     62    ("-k" magit-mergetool.keepTemporaries)
     63    ("-w" magit-mergetool.writeToTemp)]
     64   ["Actions"
     65    (" m" "Invoke mergetool" magit-git-mergetool)]
     66   (interactive
     67    (if (and (not (eq transient-current-prefix 'magit-git-mergetool))
     68             current-prefix-arg)
     69        (list nil nil t)
     70      (list (magit-read-unmerged-file "Resolve")
     71            (transient-args 'magit-git-mergetool))))
     72   (if transient
     73       (transient-setup 'magit-git-mergetool)
     74     (magit-run-git-async "mergetool" "--gui" args "--" file)))
     75 
     76 (transient-define-infix magit-git-mergetool:--tool ()
     77   :description "Override mergetool"
     78   :class 'transient-option
     79   :shortarg "-t"
     80   :argument "--tool="
     81   :reader #'magit--read-mergetool)
     82 
     83 (transient-define-infix magit-merge.guitool ()
     84   :class 'magit--git-variable
     85   :variable "merge.guitool"
     86   :global t
     87   :reader #'magit--read-mergetool)
     88 
     89 (transient-define-infix magit-merge.tool ()
     90   :class 'magit--git-variable
     91   :variable "merge.tool"
     92   :global t
     93   :reader #'magit--read-mergetool)
     94 
     95 (defun magit--read-mergetool (prompt _initial-input history)
     96   (let ((choices nil)
     97         (lines (cdr (magit-git-lines "mergetool" "--tool-help"))))
     98     (while (string-prefix-p "\t\t" (car lines))
     99       (push (substring (pop lines) 2) choices))
    100     (setq choices (nreverse choices))
    101     (magit-completing-read (or prompt "Select mergetool")
    102                            choices nil t nil history)))
    103 
    104 (transient-define-infix magit-mergetool.hideResolved ()
    105   :class 'magit--git-variable:boolean
    106   :variable "mergetool.hideResolved"
    107   :default "false"
    108   :global t)
    109 
    110 (transient-define-infix magit-mergetool.keepBackup ()
    111   :class 'magit--git-variable:boolean
    112   :variable "mergetool.keepBackup"
    113   :default "true"
    114   :global t)
    115 
    116 (transient-define-infix magit-mergetool.keepTemporaries ()
    117   :class 'magit--git-variable:boolean
    118   :variable "mergetool.keepTemporaries"
    119   :default "false"
    120   :global t)
    121 
    122 (transient-define-infix magit-mergetool.writeToTemp ()
    123   :class 'magit--git-variable:boolean
    124   :variable "mergetool.writeToTemp"
    125   :default "false"
    126   :global t)
    127 
    128 ;;;; Git-Gui
    129 
    130 ;;;###autoload
    131 (defun magit-run-git-gui-blame (commit filename &optional linenum)
    132   "Run `git gui blame' on the given FILENAME and COMMIT.
    133 Interactively run it for the current file and the `HEAD', with a
    134 prefix or when the current file cannot be determined let the user
    135 choose.  When the current buffer is visiting FILENAME instruct
    136 blame to center around the line point is on."
    137   (interactive
    138    (let (revision filename)
    139      (when (or current-prefix-arg
    140                (progn
    141                  (setq revision "HEAD")
    142                  (not (setq filename (magit-file-relative-name nil 'tracked)))))
    143        (setq revision (magit-read-branch-or-commit "Blame from revision"))
    144        (setq filename (magit-read-file-from-rev revision "Blame file")))
    145      (list revision filename
    146            (and (equal filename
    147                        (ignore-errors
    148                          (magit-file-relative-name buffer-file-name)))
    149                 (line-number-at-pos)))))
    150   (magit-with-toplevel
    151     (magit-process-git 0 "gui" "blame"
    152                        (and linenum (list (format "--line=%d" linenum)))
    153                        commit
    154                        filename)))
    155 
    156 ;;;; Gitk
    157 
    158 (defcustom magit-gitk-executable
    159   (or (and (eq system-type 'windows-nt)
    160            (let ((exe (magit-git-string
    161                        "-c" "alias.X=!x() { which \"$1\" | cygpath -mf -; }; x"
    162                        "X" "gitk.exe")))
    163              (and exe (file-executable-p exe) exe)))
    164       (executable-find "gitk") "gitk")
    165   "The Gitk executable."
    166   :group 'magit-extras
    167   :set-after '(magit-git-executable)
    168   :type 'string)
    169 
    170 ;;;###autoload
    171 (defun magit-run-git-gui ()
    172   "Run `git gui' for the current git repository."
    173   (interactive)
    174   (magit-with-toplevel (magit-process-git 0 "gui")))
    175 
    176 ;;;###autoload
    177 (defun magit-run-gitk ()
    178   "Run `gitk' in the current repository."
    179   (interactive)
    180   (magit-process-file magit-gitk-executable nil 0))
    181 
    182 ;;;###autoload
    183 (defun magit-run-gitk-branches ()
    184   "Run `gitk --branches' in the current repository."
    185   (interactive)
    186   (magit-process-file magit-gitk-executable nil 0 nil "--branches"))
    187 
    188 ;;;###autoload
    189 (defun magit-run-gitk-all ()
    190   "Run `gitk --all' in the current repository."
    191   (interactive)
    192   (magit-process-file magit-gitk-executable nil 0 nil "--all"))
    193 
    194 ;;; Emacs Tools
    195 
    196 ;;;###autoload
    197 (defun ido-enter-magit-status ()
    198   "Drop into `magit-status' from file switching.
    199 
    200 To make this command available use something like:
    201 
    202   (keymap-set ido-common-completion-map
    203               \"C-x g\" \\='ido-enter-magit-status)
    204 
    205 This command does not work in Emacs 26.1.
    206 See https://github.com/magit/magit/issues/3634
    207 and https://debbugs.gnu.org/cgi/bugreport.cgi?bug=31707."
    208   (interactive)
    209   (setq ido-exit 'fallback)
    210   (setq ido-fallback #'magit-status)
    211   (exit-minibuffer))
    212 
    213 ;;;###autoload
    214 (defun magit-project-status ()
    215   "Run `magit-status' in the current project's root."
    216   (interactive)
    217   (if (fboundp 'project-root)
    218       (magit-status-setup-buffer (project-root (project-current t)))
    219     (user-error "`magit-project-status' requires `project' 0.3.0 or greater")))
    220 
    221 (defvar magit-bind-magit-project-status t
    222   "Whether to bind \"m\" to `magit-project-status' in `project-prefix-map'.
    223 If so, then an entry is added to `project-switch-commands' as
    224 well.  If you want to use another key, then you must set this
    225 to nil before loading Magit to prevent \"m\" from being bound.")
    226 
    227 (with-eval-after-load 'project
    228   (when (and magit-bind-magit-project-status
    229              ;; Added in Emacs 28.1.
    230              (boundp 'project-prefix-map)
    231              (boundp 'project-switch-commands)
    232              ;; Only modify if it hasn't already been modified.
    233              (equal project-switch-commands
    234                     (eval (car (get 'project-switch-commands 'standard-value))
    235                           t)))
    236     (keymap-set project-prefix-map "m" #'magit-project-status)
    237     (add-to-list 'project-switch-commands '(magit-project-status "Magit") t)))
    238 
    239 ;;;###autoload
    240 (defun magit-dired-jump (&optional other-window)
    241   "Visit file at point using Dired.
    242 With a prefix argument, visit in another window.  If there
    243 is no file at point, then instead visit `default-directory'."
    244   (interactive "P")
    245   (dired-jump other-window
    246               (and-let* ((file (magit-file-at-point)))
    247                 (expand-file-name (if (file-directory-p file)
    248                                       (file-name-as-directory file)
    249                                     file)))))
    250 
    251 ;;;###autoload
    252 (defun magit-dired-log (&optional follow)
    253   "Show log for all marked files, or the current file."
    254   (interactive "P")
    255   (if-let ((topdir (magit-toplevel default-directory)))
    256       (let ((args (car (magit-log-arguments)))
    257             (files (dired-get-marked-files nil nil #'magit-file-tracked-p)))
    258         (unless files
    259           (user-error "No marked file is being tracked by Git"))
    260         (when (and follow
    261                    (not (member "--follow" args))
    262                    (not (cdr files)))
    263           (push "--follow" args))
    264         (magit-log-setup-buffer
    265          (list (or (magit-get-current-branch) "HEAD"))
    266          args
    267          (let ((default-directory topdir))
    268            (mapcar #'file-relative-name files))
    269          magit-log-buffer-file-locked))
    270     (magit--not-inside-repository-error)))
    271 
    272 ;;;###autoload
    273 (defun magit-dired-am-apply-patches (repo &optional arg)
    274   "In Dired, apply the marked (or next ARG) files as patches.
    275 If inside a repository, then apply in that.  Otherwise prompt
    276 for a repository."
    277   (interactive (list (or (magit-toplevel)
    278                          (magit-read-repository t))
    279                      current-prefix-arg))
    280   ;; Note: The ERROR argument of `dired-get-marked-files' isn't
    281   ;; available until Emacs 27.
    282   (let ((files (or (dired-get-marked-files nil arg)
    283                    (user-error "No files specified"))))
    284     (magit-status-setup-buffer repo)
    285     (magit-am-apply-patches files)))
    286 
    287 ;;;###autoload
    288 (defun magit-do-async-shell-command (file)
    289   "Open FILE with `dired-do-async-shell-command'.
    290 Interactively, open the file at point."
    291   (interactive (list (or (magit-file-at-point)
    292                          (magit-read-file "Act on file"))))
    293   (require 'dired-aux)
    294   (dired-do-async-shell-command
    295    (dired-read-shell-command "& on %s: " current-prefix-arg (list file))
    296    nil (list file)))
    297 
    298 ;;; Shift Selection
    299 
    300 (defun magit--turn-on-shift-select-mode-p ()
    301   (and shift-select-mode
    302        this-command-keys-shift-translated
    303        (not mark-active)
    304        (not (eq (car-safe transient-mark-mode) 'only))))
    305 
    306 ;;;###autoload
    307 (defun magit-previous-line (&optional arg try-vscroll)
    308   "Like `previous-line' but with Magit-specific shift-selection.
    309 
    310 Magit's selection mechanism is based on the region but selects an
    311 area that is larger than the region.  This causes `previous-line'
    312 when invoked while holding the shift key to move up one line and
    313 thereby select two lines.  When invoked inside a hunk body this
    314 command does not move point on the first invocation and thereby
    315 it only selects a single line.  Which inconsistency you prefer
    316 is a matter of preference."
    317   (declare (interactive-only
    318             "use `forward-line' with negative argument instead."))
    319   (interactive "p\np")
    320   (unless arg (setq arg 1))
    321   (let ((stay (or (magit-diff-inside-hunk-body-p)
    322                   (magit-section-position-in-heading-p))))
    323     (if (and stay (= arg 1) (magit--turn-on-shift-select-mode-p))
    324         (push-mark nil nil t)
    325       (with-no-warnings
    326         (handle-shift-selection)
    327         (previous-line (if stay (max (1- arg) 1) arg) try-vscroll)))))
    328 
    329 ;;;###autoload
    330 (defun magit-next-line (&optional arg try-vscroll)
    331   "Like `next-line' but with Magit-specific shift-selection.
    332 
    333 Magit's selection mechanism is based on the region but selects
    334 an area that is larger than the region.  This causes `next-line'
    335 when invoked while holding the shift key to move down one line
    336 and thereby select two lines.  When invoked inside a hunk body
    337 this command does not move point on the first invocation and
    338 thereby it only selects a single line.  Which inconsistency you
    339 prefer is a matter of preference."
    340   (declare (interactive-only forward-line))
    341   (interactive "p\np")
    342   (unless arg (setq arg 1))
    343   (let ((stay (or (magit-diff-inside-hunk-body-p)
    344                   (magit-section-position-in-heading-p))))
    345     (if (and stay (= arg 1) (magit--turn-on-shift-select-mode-p))
    346         (push-mark nil nil t)
    347       (with-no-warnings
    348         (handle-shift-selection)
    349         (next-line (if stay (max (1- arg) 1) arg) try-vscroll)))))
    350 
    351 ;;; Clean
    352 
    353 ;;;###autoload
    354 (defun magit-clean (&optional arg)
    355   "Remove untracked files from the working tree.
    356 With a prefix argument also remove ignored files,
    357 with two prefix arguments remove ignored files only.
    358 \n(git clean -f -d [-x|-X])"
    359   (interactive "p")
    360   (when (yes-or-no-p (format "Remove %s files? "
    361                              (pcase arg
    362                                (1 "untracked")
    363                                (4 "untracked and ignored")
    364                                (_ "ignored"))))
    365     (magit-wip-commit-before-change)
    366     (magit-run-git "clean" "-f" "-d" (pcase arg (4 "-x") (16 "-X")))))
    367 
    368 (put 'magit-clean 'disabled t)
    369 
    370 ;;; ChangeLog
    371 
    372 ;;;###autoload
    373 (defun magit-generate-changelog (&optional amending)
    374   "Insert ChangeLog entries into the current buffer.
    375 
    376 The entries are generated from the diff being committed.
    377 If prefix argument, AMENDING, is non-nil, include changes
    378 in HEAD as well as staged changes in the diff to check."
    379   (interactive "P")
    380   (unless (magit-commit-message-buffer)
    381     (user-error "No commit in progress"))
    382   (require 'diff-mode) ; `diff-add-log-current-defuns'.
    383   (require 'vc-git)    ; `vc-git-diff'.
    384   (require 'add-log)   ; `change-log-insert-entries'.
    385   (cond
    386    ((and (fboundp 'change-log-insert-entries)
    387          (fboundp 'diff-add-log-current-defuns))
    388     (setq default-directory
    389           (if (and (file-regular-p "gitdir")
    390                    (not (magit-git-true "rev-parse" "--is-inside-work-tree"))
    391                    (magit-git-true "rev-parse" "--is-inside-git-dir"))
    392               (file-name-directory (magit-file-line "gitdir"))
    393             (magit-toplevel)))
    394     (let ((rev1 (if amending "HEAD^1" "HEAD"))
    395           (rev2 nil))
    396       ;; Magit may have updated the files without notifying vc, but
    397       ;; `diff-add-log-current-defuns' relies on vc being up-to-date.
    398       (mapc #'vc-file-clearprops (magit-staged-files))
    399       (change-log-insert-entries
    400        (with-temp-buffer
    401          (vc-git-command (current-buffer) 1 nil
    402                          "diff-index" "--exit-code" "--patch"
    403                          (and (magit-anything-staged-p) "--cached")
    404                          rev1 "--")
    405          ;; `diff-find-source-location' consults these vars.
    406          (defvar diff-vc-revisions)
    407          (setq-local diff-vc-revisions (list rev1 rev2))
    408          (setq-local diff-vc-backend 'Git)
    409          (diff-add-log-current-defuns)))))
    410    ((user-error "`magit-generate-changelog' requires Emacs 27 or greater"))))
    411 
    412 ;;;###autoload
    413 (defun magit-add-change-log-entry (&optional whoami file-name other-window)
    414   "Find change log file and add date entry and item for current change.
    415 This differs from `add-change-log-entry' (which see) in that
    416 it acts on the current hunk in a Magit buffer instead of on
    417 a position in a file-visiting buffer."
    418   (interactive (list current-prefix-arg
    419                      (prompt-for-change-log-name)))
    420   (pcase-let ((`(,buf ,pos) (magit-diff-visit-file--noselect)))
    421     (magit--with-temp-position buf pos
    422       (let ((add-log-buffer-file-name-function
    423              (lambda ()
    424                (or magit-buffer-file-name
    425                    (buffer-file-name)))))
    426         (add-change-log-entry whoami file-name other-window)))))
    427 
    428 ;;;###autoload
    429 (defun magit-add-change-log-entry-other-window (&optional whoami file-name)
    430   "Find change log file in other window and add entry and item.
    431 This differs from `add-change-log-entry-other-window' (which see)
    432 in that it acts on the current hunk in a Magit buffer instead of
    433 on a position in a file-visiting buffer."
    434   (interactive (and current-prefix-arg
    435                     (list current-prefix-arg
    436                           (prompt-for-change-log-name))))
    437   (magit-add-change-log-entry whoami file-name t))
    438 
    439 ;;; Edit Line Commit
    440 
    441 ;;;###autoload
    442 (defun magit-edit-line-commit (&optional type)
    443   "Edit the commit that added the current line.
    444 
    445 With a prefix argument edit the commit that removes the line,
    446 if any.  The commit is determined using `git blame' and made
    447 editable using `git rebase --interactive' if it is reachable
    448 from `HEAD', or by checking out the commit (or a branch that
    449 points at it) otherwise."
    450   (interactive (list (and current-prefix-arg 'removal)))
    451   (let* ((chunk (magit-current-blame-chunk (or type 'addition)))
    452          (rev   (oref chunk orig-rev)))
    453     (if (string-match-p "\\`0\\{40,\\}\\'" rev)
    454         (message "This line has not been committed yet")
    455       (let ((rebase (magit-rev-ancestor-p rev "HEAD"))
    456             (file   (expand-file-name (oref chunk orig-file)
    457                                       (magit-toplevel))))
    458         (if rebase
    459             (let ((magit--rebase-published-symbol 'edit-published))
    460               (magit-rebase-edit-commit rev (magit-rebase-arguments)))
    461           (magit--checkout (or (magit-rev-branch rev) rev)))
    462         (unless (and buffer-file-name
    463                      (file-equal-p file buffer-file-name))
    464           (let ((blame-type (and magit-blame-mode magit-blame-type)))
    465             (if rebase
    466                 (set-process-sentinel
    467                  magit-this-process
    468                  (lambda (process event)
    469                    (magit-sequencer-process-sentinel process event)
    470                    (when (eq (process-status process) 'exit)
    471                      (find-file file)
    472                      (when blame-type
    473                        (magit-blame--pre-blame-setup blame-type)
    474                        (magit-blame--run (magit-blame-arguments))))))
    475               (find-file file)
    476               (when blame-type
    477                 (magit-blame--pre-blame-setup blame-type)
    478                 (magit-blame--run (magit-blame-arguments))))))))))
    479 
    480 (put 'magit-edit-line-commit 'disabled t)
    481 
    482 ;;;###autoload
    483 (defun magit-diff-edit-hunk-commit (file)
    484   "From a hunk, edit the respective commit and visit the file.
    485 
    486 First visit the file being modified by the hunk at the correct
    487 location using `magit-diff-visit-file'.  This actually visits a
    488 blob.  When point is on a diff header, not within an individual
    489 hunk, then this visits the blob the first hunk is about.
    490 
    491 Then invoke `magit-edit-line-commit', which uses an interactive
    492 rebase to make the commit editable, or if that is not possible
    493 because the commit is not reachable from `HEAD' by checking out
    494 that commit directly.  This also causes the actual worktree file
    495 to be visited.
    496 
    497 Neither the blob nor the file buffer are killed when finishing
    498 the rebase.  If that is undesirable, then it might be better to
    499 use `magit-rebase-edit-commit' instead of this command."
    500   (interactive (list (magit-file-at-point t t)))
    501   (let ((magit-diff-visit-previous-blob nil))
    502     (with-current-buffer
    503         (magit-diff-visit-file--internal file nil #'pop-to-buffer-same-window)
    504       (magit-edit-line-commit))))
    505 
    506 (put 'magit-diff-edit-hunk-commit 'disabled t)
    507 
    508 ;;; Reshelve
    509 
    510 (defcustom magit-reshelve-since-committer-only nil
    511   "Whether `magit-reshelve-since' changes only the committer dates.
    512 Otherwise the author dates are also changed."
    513   :package-version '(magit . "3.0.0")
    514   :group 'magit-commands
    515   :type 'boolean)
    516 
    517 ;;;###autoload
    518 (defun magit-reshelve-since (rev keyid)
    519   "Change the author and committer dates of the commits since REV.
    520 
    521 Ask the user for the first reachable commit whose dates should
    522 be changed.  Then read the new date for that commit.  The initial
    523 minibuffer input and the previous history element offer good
    524 values.  The next commit will be created one minute later and so
    525 on.
    526 
    527 This command is only intended for interactive use and should only
    528 be used on highly rearranged and unpublished history.
    529 
    530 If KEYID is non-nil, then use that to sign all reshelved commits.
    531 Interactively use the value of the \"--gpg-sign\" option in the
    532 list returned by `magit-rebase-arguments'."
    533   (interactive (list nil
    534                      (transient-arg-value "--gpg-sign="
    535                                           (magit-rebase-arguments))))
    536   (let* ((current (or (magit-get-current-branch)
    537                       (user-error "Refusing to reshelve detached head")))
    538          (backup (concat "refs/original/refs/heads/" current)))
    539     (cond
    540      ((not rev)
    541       (when (and (magit-ref-p backup)
    542                  (not (magit-y-or-n-p
    543                        (format "Backup ref %s already exists.  Override? "
    544                                backup))))
    545         (user-error "Abort"))
    546       (magit-log-select
    547         (lambda (rev)
    548           (magit-reshelve-since rev keyid))
    549         "Type %p on a commit to reshelve it and the commits above it,"))
    550      (t
    551       (cl-flet ((adjust (time offset)
    552                   (format-time-string
    553                    "%F %T %z"
    554                    (+ (floor time)
    555                       (* offset 60)
    556                       (- (car (decode-time time)))))))
    557         (let* ((start (concat rev "^"))
    558                (range (concat start ".." current))
    559                (time-rev (adjust (float-time (string-to-number
    560                                               (magit-rev-format "%at" start)))
    561                                  1))
    562                (time-now (adjust (float-time)
    563                                  (- (string-to-number
    564                                      (magit-git-string "rev-list" "--count"
    565                                                        range))))))
    566           (push time-rev magit--reshelve-history)
    567           (let ((date (floor
    568                        (float-time
    569                         (date-to-time
    570                          (read-string "Date for first commit: "
    571                                       time-now 'magit--reshelve-history))))))
    572             (with-environment-variables (("FILTER_BRANCH_SQUELCH_WARNING" "1"))
    573               (magit-with-toplevel
    574                 (magit-run-git-async
    575                  "filter-branch" "--force" "--env-filter"
    576                  (format
    577                   "case $GIT_COMMIT in %s\nesac"
    578                   (mapconcat
    579                    (lambda (rev)
    580                      (prog1
    581                          (concat
    582                           (format "%s) " rev)
    583                           (and (not magit-reshelve-since-committer-only)
    584                                (format "export GIT_AUTHOR_DATE=\"%s\"; " date))
    585                           (format "export GIT_COMMITTER_DATE=\"%s\";;" date))
    586                        (cl-incf date 60)))
    587                    (magit-git-lines "rev-list" "--reverse" range)
    588                    " "))
    589                  (and keyid
    590                       (list "--commit-filter"
    591                             (format "git commit-tree --gpg-sign=%s \"$@\";"
    592                                     keyid)))
    593                  range "--"))
    594               (set-process-sentinel
    595                magit-this-process
    596                (lambda (process event)
    597                  (when (memq (process-status process) '(exit signal))
    598                    (if (> (process-exit-status process) 0)
    599                        (magit-process-sentinel process event)
    600                      (process-put process 'inhibit-refresh t)
    601                      (magit-process-sentinel process event)
    602                      (magit-run-git "update-ref" "-d" backup)))))))))))))
    603 
    604 ;;; Revision Stack
    605 
    606 (defvar magit-revision-stack nil)
    607 
    608 (defcustom magit-pop-revision-stack-format
    609   '("[%N: %h] "
    610     "%N: %cs %H\n   %s\n"
    611     "\\[\\([0-9]+\\)[]:]")
    612   "Control how `magit-pop-revision-stack' inserts a revision.
    613 
    614 The command `magit-pop-revision-stack' inserts a representation
    615 of the revision last pushed to the `magit-revision-stack' into
    616 the current buffer.  It inserts text at point and/or near the end
    617 of the buffer, and removes the consumed revision from the stack.
    618 
    619 The entries on the stack have the format (HASH TOPLEVEL) and this
    620 option has the format (POINT-FORMAT EOB-FORMAT INDEX-REGEXP), all
    621 of which may be nil or a string (though either one of EOB-FORMAT
    622 or POINT-FORMAT should be a string, and if INDEX-REGEXP is
    623 non-nil, then the two formats should be too).
    624 
    625 First INDEX-REGEXP is used to find the previously inserted entry,
    626 by searching backward from point.  The first submatch must match
    627 the index number.  That number is incremented by one, and becomes
    628 the index number of the entry to be inserted.  If you don't want
    629 to number the inserted revisions, then use nil for INDEX-REGEXP.
    630 
    631 If INDEX-REGEXP is non-nil, then both POINT-FORMAT and EOB-FORMAT
    632 should contain \"%N\", which is replaced with the number that was
    633 determined in the previous step.
    634 
    635 Both formats, if non-nil and after removing %N, are then expanded
    636 using `git show --format=FORMAT ...' inside TOPLEVEL.
    637 
    638 The expansion of POINT-FORMAT is inserted at point, and the
    639 expansion of EOB-FORMAT is inserted at the end of the buffer (if
    640 the buffer ends with a comment, then it is inserted right before
    641 that)."
    642   :package-version '(magit . "3.2.0")
    643   :group 'magit-commands
    644   :type '(list (choice (string :tag "Insert at point format")
    645                        (cons (string :tag "Insert at point format")
    646                              (repeat (string :tag "Argument to git show")))
    647                        (const :tag "Don't insert at point" nil))
    648                (choice (string :tag "Insert at eob format")
    649                        (cons (string :tag "Insert at eob format")
    650                              (repeat (string :tag "Argument to git show")))
    651                        (const :tag "Don't insert at eob" nil))
    652                (choice (regexp :tag "Find index regexp")
    653                        (const :tag "Don't number entries" nil))))
    654 
    655 (defcustom magit-copy-revision-abbreviated nil
    656   "Whether to save abbreviated revision to `kill-ring' and `magit-revision-stack'."
    657   :package-version '(magit . "3.0.0")
    658   :group 'magit-miscellaneous
    659   :type 'boolean)
    660 
    661 ;;;###autoload
    662 (defun magit-pop-revision-stack (rev toplevel)
    663   "Insert a representation of a revision into the current buffer.
    664 
    665 Pop a revision from the `magit-revision-stack' and insert it into
    666 the current buffer according to `magit-pop-revision-stack-format'.
    667 Revisions can be put on the stack using `magit-copy-section-value'
    668 and `magit-copy-buffer-revision'.
    669 
    670 If the stack is empty or with a prefix argument, instead read a
    671 revision in the minibuffer.  By using the minibuffer history this
    672 allows selecting an item which was popped earlier or to insert an
    673 arbitrary reference or revision without first pushing it onto the
    674 stack.
    675 
    676 When reading the revision from the minibuffer, then it might not
    677 be possible to guess the correct repository.  When this command
    678 is called inside a repository (e.g., while composing a commit
    679 message), then that repository is used.  Otherwise (e.g., while
    680 composing an email) then the repository recorded for the top
    681 element of the stack is used (even though we insert another
    682 revision).  If not called inside a repository and with an empty
    683 stack, or with two prefix arguments, then read the repository in
    684 the minibuffer too."
    685   (interactive
    686    (if (or current-prefix-arg (not magit-revision-stack))
    687        (let ((default-directory
    688               (or (and (not (= (prefix-numeric-value current-prefix-arg) 16))
    689                        (or (magit-toplevel)
    690                            (cadr (car magit-revision-stack))))
    691                   (magit-read-repository))))
    692          (list (magit-read-branch-or-commit "Insert revision")
    693                default-directory))
    694      (push (caar magit-revision-stack) magit-revision-history)
    695      (pop magit-revision-stack)))
    696   (if rev
    697       (pcase-let ((`(,pnt-format ,eob-format ,idx-format)
    698                    magit-pop-revision-stack-format))
    699         (let ((default-directory toplevel)
    700               (idx (and idx-format
    701                         (save-excursion
    702                           (if (re-search-backward idx-format nil t)
    703                               (number-to-string
    704                                (1+ (string-to-number (match-string 1))))
    705                             "1"))))
    706               pnt-args eob-args)
    707           (when (listp pnt-format)
    708             (setq pnt-args (cdr pnt-format))
    709             (setq pnt-format (car pnt-format)))
    710           (when (listp eob-format)
    711             (setq eob-args (cdr eob-format))
    712             (setq eob-format (car eob-format)))
    713           (when pnt-format
    714             (when idx-format
    715               (setq pnt-format
    716                     (string-replace "%N" idx pnt-format)))
    717             (magit-rev-insert-format pnt-format rev pnt-args)
    718             (delete-char -1))
    719           (when eob-format
    720             (when idx-format
    721               (setq eob-format
    722                     (string-replace "%N" idx eob-format)))
    723             (save-excursion
    724               (goto-char (point-max))
    725               (skip-syntax-backward ">-")
    726               (beginning-of-line)
    727               (if (and comment-start (looking-at comment-start))
    728                   (while (looking-at comment-start)
    729                     (forward-line -1))
    730                 (forward-line)
    731                 (unless (= (current-column) 0)
    732                   (insert ?\n)))
    733               (insert ?\n)
    734               (magit-rev-insert-format eob-format rev eob-args)
    735               (delete-char -1)))))
    736     (user-error "Revision stack is empty")))
    737 
    738 (keymap-set git-commit-mode-map "C-c C-w" #'magit-pop-revision-stack)
    739 
    740 ;;;###autoload
    741 (defun magit-copy-section-value (arg)
    742   "Save the value of the current section for later use.
    743 
    744 Save the section value to the `kill-ring', and, provided that
    745 the current section is a commit, branch, or tag section, push
    746 the (referenced) revision to the `magit-revision-stack' for use
    747 with `magit-pop-revision-stack'.
    748 
    749 When `magit-copy-revision-abbreviated' is non-nil, save the
    750 abbreviated revision to the `kill-ring' and the
    751 `magit-revision-stack'.
    752 
    753 When the current section is a branch or a tag, and a prefix
    754 argument is used, then save the revision at its tip to the
    755 `kill-ring' instead of the reference name.
    756 
    757 When the region is active, then save that to the `kill-ring',
    758 like `kill-ring-save' would, instead of behaving as described
    759 above.  If a prefix argument is used and the region is within
    760 a hunk, then strip the diff marker column and keep only either
    761 the added or removed lines, depending on the sign of the prefix
    762 argument."
    763   (interactive "P")
    764   (cond
    765    ((and arg
    766          (magit-section-internal-region-p)
    767          (magit-section-match 'hunk))
    768     (kill-new
    769      (thread-last (buffer-substring-no-properties
    770                    (region-beginning)
    771                    (region-end))
    772        (replace-regexp-in-string
    773         (format "^\\%c.*\n?" (if (< (prefix-numeric-value arg) 0) ?+ ?-))
    774         "")
    775        (replace-regexp-in-string "^[ +-]" "")))
    776     (deactivate-mark))
    777    ((use-region-p)
    778     (call-interactively #'copy-region-as-kill))
    779    (t
    780     (when-let* ((section (magit-current-section))
    781                 (value (oref section value)))
    782       (magit-section-case
    783         ((branch commit module-commit tag)
    784          (let ((default-directory default-directory) ref)
    785            (magit-section-case
    786              ((branch tag)
    787               (setq ref value))
    788              (module-commit
    789               (setq default-directory
    790                     (file-name-as-directory
    791                      (expand-file-name (magit-section-parent-value section)
    792                                        (magit-toplevel))))))
    793            (setq value (magit-rev-parse
    794                         (and magit-copy-revision-abbreviated "--short")
    795                         value))
    796            (push (list value default-directory) magit-revision-stack)
    797            (kill-new (message "%s" (or (and current-prefix-arg ref)
    798                                        value)))))
    799         ((kill-new (message "%s" value))))))))
    800 
    801 ;;;###autoload
    802 (defun magit-copy-buffer-revision ()
    803   "Save the revision of the current buffer for later use.
    804 
    805 Save the revision shown in the current buffer to the `kill-ring'
    806 and push it to the `magit-revision-stack'.
    807 
    808 This command is mainly intended for use in `magit-revision-mode'
    809 buffers, the only buffers where it is always unambiguous exactly
    810 which revision should be saved.
    811 
    812 Most other Magit buffers usually show more than one revision, in
    813 some way or another, so this command has to select one of them,
    814 and that choice might not always be the one you think would have
    815 been the best pick.
    816 
    817 In such buffers it is often more useful to save the value of
    818 the current section instead, using `magit-copy-section-value'.
    819 
    820 When the region is active, then save that to the `kill-ring',
    821 like `kill-ring-save' would, instead of behaving as described
    822 above.
    823 
    824 When `magit-copy-revision-abbreviated' is non-nil, save the
    825 abbreviated revision to the `kill-ring' and the
    826 `magit-revision-stack'."
    827   (interactive)
    828   (if (use-region-p)
    829       (call-interactively #'copy-region-as-kill)
    830     (when-let ((rev (or magit-buffer-revision
    831                         (cl-case major-mode
    832                           (magit-diff-mode
    833                            (if (string-match "\\.\\.\\.?\\(.+\\)"
    834                                              magit-buffer-range)
    835                                (match-string 1 magit-buffer-range)
    836                              magit-buffer-range))
    837                           (magit-status-mode "HEAD")))))
    838       (when (magit-commit-p rev)
    839         (setq rev (magit-rev-parse
    840                    (and magit-copy-revision-abbreviated "--short")
    841                    rev))
    842         (push (list rev default-directory) magit-revision-stack)
    843         (kill-new (message "%s" rev))))))
    844 
    845 ;;; Buffer Switching
    846 
    847 ;;;###autoload
    848 (defun magit-display-repository-buffer (buffer)
    849   "Display a Magit buffer belonging to the current Git repository.
    850 The buffer is displayed using `magit-display-buffer', which see."
    851   (interactive (list (magit--read-repository-buffer
    852                       "Display magit buffer: ")))
    853   (magit-display-buffer (get-buffer buffer)))
    854 
    855 ;;;###autoload
    856 (defun magit-switch-to-repository-buffer (buffer)
    857   "Switch to a Magit buffer belonging to the current Git repository."
    858   (interactive (list (magit--read-repository-buffer
    859                       "Switch to magit buffer: ")))
    860   (switch-to-buffer buffer))
    861 
    862 ;;;###autoload
    863 (defun magit-switch-to-repository-buffer-other-window (buffer)
    864   "Switch to a Magit buffer belonging to the current Git repository."
    865   (interactive (list (magit--read-repository-buffer
    866                       "Switch to magit buffer in another window: ")))
    867   (switch-to-buffer-other-window buffer))
    868 
    869 ;;;###autoload
    870 (defun magit-switch-to-repository-buffer-other-frame (buffer)
    871   "Switch to a Magit buffer belonging to the current Git repository."
    872   (interactive (list (magit--read-repository-buffer
    873                       "Switch to magit buffer in another frame: ")))
    874   (switch-to-buffer-other-frame buffer))
    875 
    876 (defun magit--read-repository-buffer (prompt)
    877   (if-let ((topdir (magit-rev-parse-safe "--show-toplevel")))
    878       (read-buffer
    879        prompt (magit-get-mode-buffer 'magit-status-mode) t
    880        (pcase-lambda (`(,_ . ,buf))
    881          (and buf
    882               (with-current-buffer buf
    883                 (and (or (derived-mode-p 'magit-mode
    884                                          'magit-repolist-mode
    885                                          'magit-submodule-list-mode
    886                                          'git-rebase-mode)
    887                          (and buffer-file-name
    888                               (string-match-p git-commit-filename-regexp
    889                                               buffer-file-name)))
    890                      (equal (magit-rev-parse-safe "--show-toplevel")
    891                             topdir))))))
    892     (user-error "Not inside a Git repository")))
    893 
    894 ;;; Miscellaneous
    895 
    896 ;;;###autoload
    897 (defun magit-abort-dwim ()
    898   "Abort current operation.
    899 Depending on the context, this will abort a merge, a rebase, a
    900 patch application, a cherry-pick, a revert, or a bisect."
    901   (interactive)
    902   (cond ((magit-merge-in-progress-p)     (magit-merge-abort))
    903         ((magit-rebase-in-progress-p)    (magit-rebase-abort))
    904         ((magit-am-in-progress-p)        (magit-am-abort))
    905         ((magit-sequencer-in-progress-p) (magit-sequencer-abort))
    906         ((magit-bisect-in-progress-p)    (magit-bisect-reset))))
    907 
    908 ;;; _
    909 (provide 'magit-extras)
    910 ;;; magit-extras.el ends here