config

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

magit-extras.el (37138B)


      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 ;;;###autoload
    739 (defun magit-copy-section-value (arg)
    740   "Save the value of the current section for later use.
    741 
    742 Save the section value to the `kill-ring', and, provided that
    743 the current section is a commit, branch, or tag section, push
    744 the (referenced) revision to the `magit-revision-stack' for use
    745 with `magit-pop-revision-stack'.
    746 
    747 When `magit-copy-revision-abbreviated' is non-nil, save the
    748 abbreviated revision to the `kill-ring' and the
    749 `magit-revision-stack'.
    750 
    751 When the current section is a branch or a tag, and a prefix
    752 argument is used, then save the revision at its tip to the
    753 `kill-ring' instead of the reference name.
    754 
    755 When the region is active, then save that to the `kill-ring',
    756 like `kill-ring-save' would, instead of behaving as described
    757 above.  If a prefix argument is used and the region is within
    758 a hunk, then strip the diff marker column and keep only either
    759 the added or removed lines, depending on the sign of the prefix
    760 argument."
    761   (interactive "P")
    762   (cond
    763    ((and arg
    764          (magit-section-internal-region-p)
    765          (magit-section-match 'hunk))
    766     (kill-new
    767      (thread-last (buffer-substring-no-properties
    768                    (region-beginning)
    769                    (region-end))
    770        (replace-regexp-in-string
    771         (format "^\\%c.*\n?" (if (< (prefix-numeric-value arg) 0) ?+ ?-))
    772         "")
    773        (replace-regexp-in-string "^[ +-]" "")))
    774     (deactivate-mark))
    775    ((use-region-p)
    776     (call-interactively #'copy-region-as-kill))
    777    (t
    778     (when-let* ((section (magit-current-section))
    779                 (value (oref section value)))
    780       (magit-section-case
    781         ((branch commit module-commit tag)
    782          (let ((default-directory default-directory) ref)
    783            (magit-section-case
    784              ((branch tag)
    785               (setq ref value))
    786              (module-commit
    787               (setq default-directory
    788                     (file-name-as-directory
    789                      (expand-file-name (magit-section-parent-value section)
    790                                        (magit-toplevel))))))
    791            (setq value (magit-rev-parse
    792                         (and magit-copy-revision-abbreviated "--short")
    793                         value))
    794            (push (list value default-directory) magit-revision-stack)
    795            (kill-new (message "%s" (or (and current-prefix-arg ref)
    796                                        value)))))
    797         (t (kill-new (message "%s" value))))))))
    798 
    799 ;;;###autoload
    800 (defun magit-copy-buffer-revision ()
    801   "Save the revision of the current buffer for later use.
    802 
    803 Save the revision shown in the current buffer to the `kill-ring'
    804 and push it to the `magit-revision-stack'.
    805 
    806 This command is mainly intended for use in `magit-revision-mode'
    807 buffers, the only buffers where it is always unambiguous exactly
    808 which revision should be saved.
    809 
    810 Most other Magit buffers usually show more than one revision, in
    811 some way or another, so this command has to select one of them,
    812 and that choice might not always be the one you think would have
    813 been the best pick.
    814 
    815 In such buffers it is often more useful to save the value of
    816 the current section instead, using `magit-copy-section-value'.
    817 
    818 When the region is active, then save that to the `kill-ring',
    819 like `kill-ring-save' would, instead of behaving as described
    820 above.
    821 
    822 When `magit-copy-revision-abbreviated' is non-nil, save the
    823 abbreviated revision to the `kill-ring' and the
    824 `magit-revision-stack'."
    825   (interactive)
    826   (if (use-region-p)
    827       (call-interactively #'copy-region-as-kill)
    828     (when-let ((rev (or magit-buffer-revision
    829                         (cl-case major-mode
    830                           (magit-diff-mode
    831                            (if (string-match "\\.\\.\\.?\\(.+\\)"
    832                                              magit-buffer-range)
    833                                (match-string 1 magit-buffer-range)
    834                              magit-buffer-range))
    835                           (magit-status-mode "HEAD")))))
    836       (when (magit-commit-p rev)
    837         (setq rev (magit-rev-parse
    838                    (and magit-copy-revision-abbreviated "--short")
    839                    rev))
    840         (push (list rev default-directory) magit-revision-stack)
    841         (kill-new (message "%s" rev))))))
    842 
    843 ;;; Buffer Switching
    844 
    845 ;;;###autoload
    846 (defun magit-display-repository-buffer (buffer)
    847   "Display a Magit buffer belonging to the current Git repository.
    848 The buffer is displayed using `magit-display-buffer', which see."
    849   (interactive (list (magit--read-repository-buffer
    850                       "Display magit buffer: ")))
    851   (magit-display-buffer (get-buffer buffer)))
    852 
    853 ;;;###autoload
    854 (defun magit-switch-to-repository-buffer (buffer)
    855   "Switch to a Magit buffer belonging to the current Git repository."
    856   (interactive (list (magit--read-repository-buffer
    857                       "Switch to magit buffer: ")))
    858   (switch-to-buffer buffer))
    859 
    860 ;;;###autoload
    861 (defun magit-switch-to-repository-buffer-other-window (buffer)
    862   "Switch to a Magit buffer belonging to the current Git repository."
    863   (interactive (list (magit--read-repository-buffer
    864                       "Switch to magit buffer in another window: ")))
    865   (switch-to-buffer-other-window buffer))
    866 
    867 ;;;###autoload
    868 (defun magit-switch-to-repository-buffer-other-frame (buffer)
    869   "Switch to a Magit buffer belonging to the current Git repository."
    870   (interactive (list (magit--read-repository-buffer
    871                       "Switch to magit buffer in another frame: ")))
    872   (switch-to-buffer-other-frame buffer))
    873 
    874 (defun magit--read-repository-buffer (prompt)
    875   (if-let ((topdir (magit-rev-parse-safe "--show-toplevel")))
    876       (read-buffer
    877        prompt (magit-get-mode-buffer 'magit-status-mode) t
    878        (pcase-lambda (`(,_ . ,buf))
    879          (and buf
    880               (with-current-buffer buf
    881                 (and (or (derived-mode-p 'magit-mode
    882                                          'magit-repolist-mode
    883                                          'magit-submodule-list-mode
    884                                          'git-rebase-mode)
    885                          (and buffer-file-name
    886                               (string-match-p git-commit-filename-regexp
    887                                               buffer-file-name)))
    888                      (equal (magit-rev-parse-safe "--show-toplevel")
    889                             topdir))))))
    890     (user-error "Not inside a Git repository")))
    891 
    892 ;;; Miscellaneous
    893 
    894 ;;;###autoload
    895 (defun magit-abort-dwim ()
    896   "Abort current operation.
    897 Depending on the context, this will abort a merge, a rebase, a
    898 patch application, a cherry-pick, a revert, or a bisect."
    899   (interactive)
    900   (cond ((magit-merge-in-progress-p)     (magit-merge-abort))
    901         ((magit-rebase-in-progress-p)    (magit-rebase-abort))
    902         ((magit-am-in-progress-p)        (magit-am-abort))
    903         ((magit-sequencer-in-progress-p) (magit-sequencer-abort))
    904         ((magit-bisect-in-progress-p)    (magit-bisect-reset))))
    905 
    906 ;;;###autoload
    907 (defun magit-back-to-indentation ()
    908   "Move point to the first non-whitespace character on this line.
    909 In Magit diffs, also skip over - and + at the beginning of the line."
    910   (interactive "^")
    911   (beginning-of-line 1)
    912   (when (and (magit-section-match 'hunk)
    913              (looking-at (if (oref (magit-current-section) combined)
    914                              "^ ?[-+]+"
    915                            "^[-+]")))
    916     (goto-char (match-end 0)))
    917   (skip-syntax-forward " " (line-end-position))
    918   (backward-prefix-chars))
    919 
    920 ;;; _
    921 (provide 'magit-extras)
    922 ;;; magit-extras.el ends here