config

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

magit-extras.el (37153B)


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