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