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