fordon_greeman / voidfiles

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