commit | author | age
|
5cb5f7
|
1 |
;;; magit-extras.el --- additional functionality for Magit -*- lexical-binding: t -*- |
C |
2 |
|
|
3 |
;; Copyright (C) 2008-2018 The Magit Project Contributors |
|
4 |
;; |
|
5 |
;; You should have received a copy of the AUTHORS.md file which |
|
6 |
;; lists all contributors. If not, see http://magit.vc/authors. |
|
7 |
|
|
8 |
;; Magit is free software; you can redistribute it and/or modify it |
|
9 |
;; under the terms of the GNU General Public License as published by |
|
10 |
;; the Free Software Foundation; either version 3, or (at your option) |
|
11 |
;; any later version. |
|
12 |
;; |
|
13 |
;; Magit is distributed in the hope that it will be useful, but WITHOUT |
|
14 |
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
|
15 |
;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public |
|
16 |
;; License for more details. |
|
17 |
;; |
|
18 |
;; You should have received a copy of the GNU General Public License |
|
19 |
;; along with Magit. If not, see http://www.gnu.org/licenses. |
|
20 |
|
|
21 |
;;; Commentary: |
|
22 |
|
|
23 |
;; Additional functionality for Magit. |
|
24 |
|
|
25 |
;;; Code: |
|
26 |
|
|
27 |
(eval-when-compile |
|
28 |
(require 'subr-x)) |
|
29 |
|
|
30 |
(require 'magit) |
|
31 |
|
|
32 |
(declare-function dired-read-shell-command "dired-aux" (prompt arg files)) |
|
33 |
|
|
34 |
(defgroup magit-extras nil |
|
35 |
"Additional functionality for Magit." |
|
36 |
:group 'magit-extensions) |
|
37 |
|
|
38 |
;;; External Tools |
|
39 |
|
|
40 |
(defcustom magit-gitk-executable |
|
41 |
(or (and (eq system-type 'windows-nt) |
|
42 |
(let ((exe (magit-git-string |
|
43 |
"-c" "alias.X=!x() { which \"$1\" | cygpath -mf -; }; x" |
|
44 |
"X" "gitk.exe"))) |
|
45 |
(and exe (file-executable-p exe) exe))) |
|
46 |
(executable-find "gitk") "gitk") |
|
47 |
"The Gitk executable." |
|
48 |
:group 'magit-extras |
|
49 |
:set-after '(magit-git-executable) |
|
50 |
:type 'string) |
|
51 |
|
|
52 |
;;;###autoload |
|
53 |
(defun magit-run-git-gui () |
|
54 |
"Run `git gui' for the current git repository." |
|
55 |
(interactive) |
|
56 |
(magit-with-toplevel |
|
57 |
(magit-process-file magit-git-executable nil 0 nil "gui"))) |
|
58 |
|
|
59 |
;;;###autoload |
|
60 |
(defun magit-run-git-gui-blame (commit filename &optional linenum) |
|
61 |
"Run `git gui blame' on the given FILENAME and COMMIT. |
|
62 |
Interactively run it for the current file and the `HEAD', with a |
|
63 |
prefix or when the current file cannot be determined let the user |
|
64 |
choose. When the current buffer is visiting FILENAME instruct |
|
65 |
blame to center around the line point is on." |
|
66 |
(interactive |
|
67 |
(let (revision filename) |
|
68 |
(when (or current-prefix-arg |
|
69 |
(not (setq revision "HEAD" |
|
70 |
filename (magit-file-relative-name nil 'tracked)))) |
|
71 |
(setq revision (magit-read-branch-or-commit "Blame from revision")) |
|
72 |
(setq filename (magit-read-file-from-rev revision "Blame file"))) |
|
73 |
(list revision filename |
|
74 |
(and (equal filename |
|
75 |
(ignore-errors |
|
76 |
(magit-file-relative-name buffer-file-name))) |
|
77 |
(line-number-at-pos))))) |
|
78 |
(magit-with-toplevel |
|
79 |
(apply #'magit-process-file magit-git-executable nil 0 nil "gui" "blame" |
|
80 |
`(,@(and linenum (list (format "--line=%d" linenum))) |
|
81 |
,commit |
|
82 |
,filename)))) |
|
83 |
|
|
84 |
;;;###autoload |
|
85 |
(defun magit-run-gitk () |
|
86 |
"Run `gitk' in the current repository." |
|
87 |
(interactive) |
|
88 |
(magit-process-file magit-gitk-executable nil 0)) |
|
89 |
|
|
90 |
;;;###autoload |
|
91 |
(defun magit-run-gitk-branches () |
|
92 |
"Run `gitk --branches' in the current repository." |
|
93 |
(interactive) |
|
94 |
(magit-process-file magit-gitk-executable nil 0 nil "--branches")) |
|
95 |
|
|
96 |
;;;###autoload |
|
97 |
(defun magit-run-gitk-all () |
|
98 |
"Run `gitk --all' in the current repository." |
|
99 |
(interactive) |
|
100 |
(magit-process-file magit-gitk-executable nil 0 nil "--all")) |
|
101 |
|
|
102 |
;;; Emacs Tools |
|
103 |
|
|
104 |
;;;###autoload |
|
105 |
(defun ido-enter-magit-status () |
|
106 |
"Drop into `magit-status' from file switching. |
|
107 |
|
|
108 |
This command does not work in Emacs 26. It does work in Emacs 25 |
|
109 |
and Emacs 27. See https://github.com/magit/magit/issues/3634 and |
|
110 |
https://debbugs.gnu.org/cgi/bugreport.cgi?bug=31707. |
|
111 |
|
|
112 |
To make this command available use something like: |
|
113 |
|
|
114 |
(add-hook \\='ido-setup-hook |
|
115 |
(lambda () |
|
116 |
(define-key ido-completion-map |
|
117 |
(kbd \"C-x g\") \\='ido-enter-magit-status))) |
|
118 |
|
|
119 |
Starting with Emacs 25.1 the Ido keymaps are defined just once |
|
120 |
instead of every time Ido is invoked, so now you can modify it |
|
121 |
like pretty much every other keymap: |
|
122 |
|
|
123 |
(define-key ido-common-completion-map |
|
124 |
(kbd \"C-x g\") \\='ido-enter-magit-status)" |
|
125 |
(interactive) |
|
126 |
(with-no-warnings ; FIXME these are internal variables |
|
127 |
(setq ido-exit 'fallback) |
|
128 |
(setq fallback 'magit-status) ; for Emacs 25 |
|
129 |
(setq ido-fallback 'magit-status)) ; for Emacs 27 |
|
130 |
(exit-minibuffer)) |
|
131 |
|
|
132 |
;;;###autoload |
|
133 |
(defun magit-dired-jump (&optional other-window) |
|
134 |
"Visit file at point using Dired. |
|
135 |
With a prefix argument, visit in another window. If there |
|
136 |
is no file at point, then instead visit `default-directory'." |
|
137 |
(interactive "P") |
|
138 |
(dired-jump other-window |
|
139 |
(when-let ((file (magit-file-at-point))) |
|
140 |
(expand-file-name (if (file-directory-p file) |
|
141 |
(file-name-as-directory file) |
|
142 |
file))))) |
|
143 |
|
|
144 |
;;;###autoload |
|
145 |
(defun magit-dired-log (&optional follow) |
|
146 |
"Show log for all marked files, or the current file." |
|
147 |
(interactive "P") |
|
148 |
(if-let ((topdir (magit-toplevel default-directory))) |
|
149 |
(let ((args (car (magit-log-arguments))) |
|
150 |
(files (dired-get-marked-files nil nil #'magit-file-tracked-p))) |
|
151 |
(unless files |
|
152 |
(user-error "No marked file is being tracked by Git")) |
|
153 |
(when (and follow |
|
154 |
(not (member "--follow" args)) |
|
155 |
(not (cdr files))) |
|
156 |
(push "--follow" args)) |
|
157 |
(magit-mode-setup-internal |
|
158 |
#'magit-log-mode |
|
159 |
(list (list (or (magit-get-current-branch) "HEAD")) |
|
160 |
args |
|
161 |
(let ((default-directory topdir)) |
|
162 |
(mapcar #'file-relative-name files))) |
|
163 |
magit-log-buffer-file-locked)) |
|
164 |
(magit--not-inside-repository-error))) |
|
165 |
|
|
166 |
;;;###autoload |
|
167 |
(defun magit-do-async-shell-command (file) |
|
168 |
"Open FILE with `dired-do-async-shell-command'. |
|
169 |
Interactively, open the file at point." |
|
170 |
(interactive (list (or (magit-file-at-point) |
|
171 |
(completing-read "Act on file: " |
|
172 |
(magit-list-files))))) |
|
173 |
(require 'dired-aux) |
|
174 |
(dired-do-async-shell-command |
|
175 |
(dired-read-shell-command "& on %s: " current-prefix-arg (list file)) |
|
176 |
nil (list file))) |
|
177 |
|
|
178 |
;;; Shift Selection |
|
179 |
|
|
180 |
(defun magit--turn-on-shift-select-mode-p () |
|
181 |
(and shift-select-mode |
|
182 |
this-command-keys-shift-translated |
|
183 |
(not mark-active) |
|
184 |
(not (eq (car-safe transient-mark-mode) 'only)))) |
|
185 |
|
|
186 |
;;;###autoload |
|
187 |
(defun magit-previous-line (&optional arg try-vscroll) |
|
188 |
"Like `previous-line' but with Magit-specific shift-selection. |
|
189 |
|
|
190 |
Magit's selection mechanism is based on the region but selects an |
|
191 |
area that is larger than the region. This causes `previous-line' |
|
192 |
when invoked while holding the shift key to move up one line and |
|
193 |
thereby select two lines. When invoked inside a hunk body this |
|
194 |
command does not move point on the first invocation and thereby |
|
195 |
it only selects a single line. Which inconsistency you prefer |
|
196 |
is a matter of preference." |
|
197 |
(declare (interactive-only |
|
198 |
"use `forward-line' with negative argument instead.")) |
|
199 |
(interactive "p\np") |
|
200 |
(unless arg (setq arg 1)) |
|
201 |
(let ((stay (or (magit-diff-inside-hunk-body-p) |
|
202 |
(magit-section-position-in-heading-p)))) |
|
203 |
(if (and stay (= arg 1) (magit--turn-on-shift-select-mode-p)) |
|
204 |
(push-mark nil nil t) |
|
205 |
(with-no-warnings |
|
206 |
(handle-shift-selection) |
|
207 |
(previous-line (if stay (max (1- arg) 1) arg) try-vscroll))))) |
|
208 |
|
|
209 |
;;;###autoload |
|
210 |
(defun magit-next-line (&optional arg try-vscroll) |
|
211 |
"Like `next-line' but with Magit-specific shift-selection. |
|
212 |
|
|
213 |
Magit's selection mechanism is based on the region but selects |
|
214 |
an area that is larger than the region. This causes `next-line' |
|
215 |
when invoked while holding the shift key to move down one line |
|
216 |
and thereby select two lines. When invoked inside a hunk body |
|
217 |
this command does not move point on the first invocation and |
|
218 |
thereby it only selects a single line. Which inconsistency you |
|
219 |
prefer is a matter of preference." |
|
220 |
(declare (interactive-only forward-line)) |
|
221 |
(interactive "p\np") |
|
222 |
(unless arg (setq arg 1)) |
|
223 |
(let ((stay (or (magit-diff-inside-hunk-body-p) |
|
224 |
(magit-section-position-in-heading-p)))) |
|
225 |
(if (and stay (= arg 1) (magit--turn-on-shift-select-mode-p)) |
|
226 |
(push-mark nil nil t) |
|
227 |
(with-no-warnings |
|
228 |
(handle-shift-selection) |
|
229 |
(next-line (if stay (max (1- arg) 1) arg) try-vscroll))))) |
|
230 |
|
|
231 |
;;; Clean |
|
232 |
|
|
233 |
;;;###autoload |
|
234 |
(defun magit-clean (&optional arg) |
|
235 |
"Remove untracked files from the working tree. |
|
236 |
With a prefix argument also remove ignored files, |
|
237 |
with two prefix arguments remove ignored files only. |
|
238 |
\n(git clean -f -d [-x|-X])" |
|
239 |
(interactive "p") |
|
240 |
(when (yes-or-no-p (format "Remove %s files? " |
|
241 |
(pcase arg |
|
242 |
(1 "untracked") |
|
243 |
(4 "untracked and ignored") |
|
244 |
(_ "ignored")))) |
|
245 |
(magit-wip-commit-before-change) |
|
246 |
(magit-run-git "clean" "-f" "-d" (pcase arg (4 "-x") (16 "-X"))))) |
|
247 |
|
|
248 |
(put 'magit-clean 'disabled t) |
|
249 |
|
|
250 |
;;; ChangeLog |
|
251 |
|
|
252 |
;;;###autoload |
|
253 |
(defun magit-add-change-log-entry (&optional whoami file-name other-window) |
|
254 |
"Find change log file and add date entry and item for current change. |
|
255 |
This differs from `add-change-log-entry' (which see) in that |
|
256 |
it acts on the current hunk in a Magit buffer instead of on |
|
257 |
a position in a file-visiting buffer." |
|
258 |
(interactive (list current-prefix-arg |
|
259 |
(prompt-for-change-log-name))) |
|
260 |
(let (buf pos) |
|
261 |
(save-window-excursion |
|
262 |
(call-interactively #'magit-diff-visit-file) |
|
263 |
(setq buf (current-buffer)) |
|
264 |
(setq pos (point))) |
|
265 |
(save-excursion |
|
266 |
(with-current-buffer buf |
|
267 |
(goto-char pos) |
|
268 |
(add-change-log-entry whoami file-name other-window))))) |
|
269 |
|
|
270 |
;;;###autoload |
|
271 |
(defun magit-add-change-log-entry-other-window (&optional whoami file-name) |
|
272 |
"Find change log file in other window and add entry and item. |
|
273 |
This differs from `add-change-log-entry-other-window' (which see) |
|
274 |
in that it acts on the current hunk in a Magit buffer instead of |
|
275 |
on a position in a file-visiting buffer." |
|
276 |
(interactive (and current-prefix-arg |
|
277 |
(list current-prefix-arg |
|
278 |
(prompt-for-change-log-name)))) |
|
279 |
(magit-add-change-log-entry whoami file-name t)) |
|
280 |
|
|
281 |
;;; Edit Line Commit |
|
282 |
|
|
283 |
;;;###autoload |
|
284 |
(defun magit-edit-line-commit (&optional type) |
|
285 |
"Edit the commit that added the current line. |
|
286 |
|
|
287 |
With a prefix argument edit the commit that removes the line, |
|
288 |
if any. The commit is determined using `git blame' and made |
|
289 |
editable using `git rebase --interactive' if it is reachable |
|
290 |
from `HEAD', or by checking out the commit (or a branch that |
|
291 |
points at it) otherwise." |
|
292 |
(interactive (list (and current-prefix-arg 'removal))) |
|
293 |
(let* ((chunk (magit-current-blame-chunk (or type 'addition))) |
|
294 |
(rev (oref chunk orig-rev))) |
|
295 |
(if (equal rev "0000000000000000000000000000000000000000") |
|
296 |
(message "This line has not been committed yet") |
|
297 |
(let ((rebase (magit-rev-ancestor-p rev "HEAD")) |
|
298 |
(file (expand-file-name (oref chunk orig-file) |
|
299 |
(magit-toplevel)))) |
|
300 |
(if rebase |
|
301 |
(let ((magit--rebase-published-symbol 'edit-published)) |
|
302 |
(magit-rebase-edit-commit rev (magit-rebase-arguments))) |
|
303 |
(magit-checkout (or (magit-rev-branch rev) rev))) |
|
304 |
(unless (and buffer-file-name |
|
305 |
(file-equal-p file buffer-file-name)) |
|
306 |
(let ((blame-type (and magit-blame-mode magit-blame-type))) |
|
307 |
(if rebase |
|
308 |
(set-process-sentinel |
|
309 |
magit-this-process |
|
310 |
(lambda (process event) |
|
311 |
(magit-sequencer-process-sentinel process event) |
|
312 |
(when (eq (process-status process) 'exit) |
|
313 |
(find-file file) |
|
314 |
(when blame-type |
|
315 |
(magit-blame--pre-blame-setup blame-type) |
|
316 |
(magit-blame--run))))) |
|
317 |
(find-file file) |
|
318 |
(when blame-type |
|
319 |
(magit-blame--pre-blame-setup blame-type) |
|
320 |
(magit-blame--run))))))))) |
|
321 |
|
|
322 |
(put 'magit-edit-line-commit 'disabled t) |
|
323 |
|
|
324 |
(defun magit-diff-edit-hunk-commit () |
|
325 |
"From a hunk, edit the respective commit and visit the file. |
|
326 |
|
|
327 |
First visit the file being modified by the hunk at the correct |
|
328 |
location using `magit-diff-visit-file'. This actually visits a |
|
329 |
blob. When point is on a diff header, not within an individual |
|
330 |
hunk, then this visits the blob the first hunk is about. |
|
331 |
|
|
332 |
Then invoke `magit-edit-line-commit', which uses an interactive |
|
333 |
rebase to make the commit editable, or if that is not possible |
|
334 |
because the commit is not reachable from `HEAD' by checking out |
|
335 |
that commit directly. This also causes the actual worktree file |
|
336 |
to be visited. |
|
337 |
|
|
338 |
Neither the blob nor the file buffer are killed when finishing |
|
339 |
the rebase. If that is undesirable, then it might be better to |
|
340 |
use `magit-rebase-edit-command' instead of this command." |
|
341 |
(interactive) |
|
342 |
(let ((magit-diff-visit-previous-blob nil)) |
|
343 |
(magit-diff-visit-file (--if-let (magit-file-at-point) |
|
344 |
(expand-file-name it) |
|
345 |
(user-error "No file at point")) |
|
346 |
nil 'switch-to-buffer)) |
|
347 |
(magit-edit-line-commit)) |
|
348 |
|
|
349 |
(put 'magit-diff-edit-hunk-commit 'disabled t) |
|
350 |
|
|
351 |
;;; Reshelve |
|
352 |
|
|
353 |
;;;###autoload |
|
354 |
(defun magit-reshelve-since (rev) |
|
355 |
"Change the author and committer dates of the commits since REV. |
|
356 |
|
|
357 |
Ask the user for the first reachable commit whose dates should |
|
358 |
be changed. The read the new date for that commit. The initial |
|
359 |
minibuffer input and the previous history element offer good |
|
360 |
values. The next commit will be created one minute later and so |
|
361 |
on. |
|
362 |
|
|
363 |
This command is only intended for interactive use and should only |
|
364 |
be used on highly rearranged and unpublished history." |
|
365 |
(interactive (list nil)) |
|
366 |
(cond |
|
367 |
((not rev) |
|
368 |
(let ((backup (concat "refs/original/refs/heads/" |
|
369 |
(magit-get-current-branch)))) |
|
370 |
(when (and (magit-ref-p backup) |
|
371 |
(not (magit-y-or-n-p |
|
372 |
"Backup ref %s already exists. Override? " backup))) |
|
373 |
(user-error "Abort"))) |
|
374 |
(magit-log-select 'magit-reshelve-since |
|
375 |
"Type %p on a commit to reshelve it and the commits above it,")) |
|
376 |
(t |
|
377 |
(cl-flet ((adjust (time offset) |
|
378 |
(format-time-string |
|
379 |
"%F %T %z" |
|
380 |
(+ (floor time) |
|
381 |
(* offset 60) |
|
382 |
(- (car (decode-time time))))))) |
|
383 |
(let* ((start (concat rev "^")) |
|
384 |
(range (concat start ".." (magit-get-current-branch))) |
|
385 |
(time-rev (adjust (float-time (string-to-number |
|
386 |
(magit-rev-format "%at" start))) |
|
387 |
1)) |
|
388 |
(time-now (adjust (float-time) |
|
389 |
(- (string-to-number |
|
390 |
(magit-git-string "rev-list" "--count" |
|
391 |
range)))))) |
|
392 |
(push time-rev magit--reshelve-history) |
|
393 |
(let ((date (floor |
|
394 |
(float-time |
|
395 |
(date-to-time |
|
396 |
(read-string "Date for first commit: " |
|
397 |
time-now 'magit--reshelve-history)))))) |
|
398 |
(magit-with-toplevel |
|
399 |
(magit-run-git-async |
|
400 |
"filter-branch" "--force" "--env-filter" |
|
401 |
(format "case $GIT_COMMIT in %s\nesac" |
|
402 |
(mapconcat (lambda (rev) |
|
403 |
(prog1 (format "%s) \ |
|
404 |
export GIT_AUTHOR_DATE=\"%s\"; \ |
|
405 |
export GIT_COMMITTER_DATE=\"%s\";;" rev date date) |
|
406 |
(cl-incf date 60))) |
|
407 |
(magit-git-lines "rev-list" "--reverse" |
|
408 |
range) |
|
409 |
" ")) |
|
410 |
range "--") |
|
411 |
(set-process-sentinel |
|
412 |
magit-this-process |
|
413 |
(lambda (process event) |
|
414 |
(when (memq (process-status process) '(exit signal)) |
|
415 |
(if (> (process-exit-status process) 0) |
|
416 |
(magit-process-sentinel process event) |
|
417 |
(process-put process 'inhibit-refresh t) |
|
418 |
(magit-process-sentinel process event) |
|
419 |
(magit-run-git "update-ref" "-d" |
|
420 |
(concat "refs/original/refs/heads/" |
|
421 |
(magit-get-current-branch)))))))))))))) |
|
422 |
|
|
423 |
;;; Revision Stack |
|
424 |
|
|
425 |
(defvar magit-revision-stack nil) |
|
426 |
|
|
427 |
(defcustom magit-pop-revision-stack-format |
|
428 |
'("[%N: %h] " "%N: %H\n %s\n" "\\[\\([0-9]+\\)[]:]") |
|
429 |
"Control how `magit-pop-revision-stack' inserts a revision. |
|
430 |
|
|
431 |
The command `magit-pop-revision-stack' inserts a representation |
|
432 |
of the revision last pushed to the `magit-revision-stack' into |
|
433 |
the current buffer. It inserts text at point and/or near the end |
|
434 |
of the buffer, and removes the consumed revision from the stack. |
|
435 |
|
|
436 |
The entries on the stack have the format (HASH TOPLEVEL) and this |
|
437 |
option has the format (POINT-FORMAT EOB-FORMAT INDEX-REGEXP), all |
|
438 |
of which may be nil or a string (though either one of EOB-FORMAT |
|
439 |
or POINT-FORMAT should be a string, and if INDEX-REGEXP is |
|
440 |
non-nil, then the two formats should be too). |
|
441 |
|
|
442 |
First INDEX-REGEXP is used to find the previously inserted entry, |
|
443 |
by searching backward from point. The first submatch must match |
|
444 |
the index number. That number is incremented by one, and becomes |
|
445 |
the index number of the entry to be inserted. If you don't want |
|
446 |
to number the inserted revisions, then use nil for INDEX-REGEXP. |
|
447 |
|
|
448 |
If INDEX-REGEXP is non-nil, then both POINT-FORMAT and EOB-FORMAT |
|
449 |
should contain \"%N\", which is replaced with the number that was |
|
450 |
determined in the previous step. |
|
451 |
|
|
452 |
Both formats, if non-nil and after removing %N, are then expanded |
|
453 |
using `git show --format=FORMAT ...' inside TOPLEVEL. |
|
454 |
|
|
455 |
The expansion of POINT-FORMAT is inserted at point, and the |
|
456 |
expansion of EOB-FORMAT is inserted at the end of the buffer (if |
|
457 |
the buffer ends with a comment, then it is inserted right before |
|
458 |
that)." |
|
459 |
:package-version '(magit . "2.3.0") |
|
460 |
:group 'magit-commands |
|
461 |
:type '(list (choice (string :tag "Insert at point format") |
|
462 |
(cons (string :tag "Insert at point format") |
|
463 |
(repeat (string :tag "Argument to git show"))) |
|
464 |
(const :tag "Don't insert at point" nil)) |
|
465 |
(choice (string :tag "Insert at eob format") |
|
466 |
(cons (string :tag "Insert at eob format") |
|
467 |
(repeat (string :tag "Argument to git show"))) |
|
468 |
(const :tag "Don't insert at eob" nil)) |
|
469 |
(choice (regexp :tag "Find index regexp") |
|
470 |
(const :tag "Don't number entries" nil)))) |
|
471 |
|
|
472 |
;;;###autoload |
|
473 |
(defun magit-pop-revision-stack (rev toplevel) |
|
474 |
"Insert a representation of a revision into the current buffer. |
|
475 |
|
|
476 |
Pop a revision from the `magit-revision-stack' and insert it into |
|
477 |
the current buffer according to `magit-pop-revision-stack-format'. |
|
478 |
Revisions can be put on the stack using `magit-copy-section-value' |
|
479 |
and `magit-copy-buffer-revision'. |
|
480 |
|
|
481 |
If the stack is empty or with a prefix argument, instead read a |
|
482 |
revision in the minibuffer. By using the minibuffer history this |
|
483 |
allows selecting an item which was popped earlier or to insert an |
|
484 |
arbitrary reference or revision without first pushing it onto the |
|
485 |
stack. |
|
486 |
|
|
487 |
When reading the revision from the minibuffer, then it might not |
|
488 |
be possible to guess the correct repository. When this command |
|
489 |
is called inside a repository (e.g. while composing a commit |
|
490 |
message), then that repository is used. Otherwise (e.g. while |
|
491 |
composing an email) then the repository recorded for the top |
|
492 |
element of the stack is used (even though we insert another |
|
493 |
revision). If not called inside a repository and with an empty |
|
494 |
stack, or with two prefix arguments, then read the repository in |
|
495 |
the minibuffer too." |
|
496 |
(interactive |
|
497 |
(if (or current-prefix-arg (not magit-revision-stack)) |
|
498 |
(let ((default-directory |
|
499 |
(or (and (not (= (prefix-numeric-value current-prefix-arg) 16)) |
|
500 |
(or (magit-toplevel) |
|
501 |
(cadr (car magit-revision-stack)))) |
|
502 |
(magit-read-repository)))) |
|
503 |
(list (magit-read-branch-or-commit "Insert revision") |
|
504 |
default-directory)) |
|
505 |
(push (caar magit-revision-stack) magit-revision-history) |
|
506 |
(pop magit-revision-stack))) |
|
507 |
(if rev |
|
508 |
(pcase-let ((`(,pnt-format ,eob-format ,idx-format) |
|
509 |
magit-pop-revision-stack-format)) |
|
510 |
(let ((default-directory toplevel) |
|
511 |
(idx (and idx-format |
|
512 |
(save-excursion |
|
513 |
(if (re-search-backward idx-format nil t) |
|
514 |
(number-to-string |
|
515 |
(1+ (string-to-number (match-string 1)))) |
|
516 |
"1")))) |
|
517 |
pnt-args eob-args) |
|
518 |
(when (listp pnt-format) |
|
519 |
(setq pnt-args (cdr pnt-format)) |
|
520 |
(setq pnt-format (car pnt-format))) |
|
521 |
(when (listp eob-format) |
|
522 |
(setq eob-args (cdr eob-format)) |
|
523 |
(setq eob-format (car eob-format))) |
|
524 |
(when pnt-format |
|
525 |
(when idx-format |
|
526 |
(setq pnt-format |
|
527 |
(replace-regexp-in-string "%N" idx pnt-format t t))) |
|
528 |
(magit-rev-insert-format pnt-format rev pnt-args) |
|
529 |
(backward-delete-char 1)) |
|
530 |
(when eob-format |
|
531 |
(when idx-format |
|
532 |
(setq eob-format |
|
533 |
(replace-regexp-in-string "%N" idx eob-format t t))) |
|
534 |
(save-excursion |
|
535 |
(goto-char (point-max)) |
|
536 |
(skip-syntax-backward ">s-") |
|
537 |
(beginning-of-line) |
|
538 |
(if (and comment-start (looking-at comment-start)) |
|
539 |
(while (looking-at comment-start) |
|
540 |
(forward-line -1)) |
|
541 |
(forward-line) |
|
542 |
(unless (= (current-column) 0) |
|
543 |
(insert ?\n))) |
|
544 |
(insert ?\n) |
|
545 |
(magit-rev-insert-format eob-format rev eob-args) |
|
546 |
(backward-delete-char 1))))) |
|
547 |
(user-error "Revision stack is empty"))) |
|
548 |
|
|
549 |
(define-key git-commit-mode-map |
|
550 |
(kbd "C-c C-w") 'magit-pop-revision-stack) |
|
551 |
|
|
552 |
;;;###autoload |
|
553 |
(defun magit-copy-section-value () |
|
554 |
"Save the value of the current section for later use. |
|
555 |
|
|
556 |
Save the section value to the `kill-ring', and, provided that |
|
557 |
the current section is a commit, branch, or tag section, push |
|
558 |
the (referenced) revision to the `magit-revision-stack' for use |
|
559 |
with `magit-pop-revision-stack'. |
|
560 |
|
|
561 |
When the current section is a branch or a tag, and a prefix |
|
562 |
argument is used, then save the revision at its tip to the |
|
563 |
`kill-ring' instead of the reference name. |
|
564 |
|
|
565 |
When the region is active, then save that to the `kill-ring', |
|
566 |
like `kill-ring-save' would, instead of behaving as described |
|
567 |
above." |
|
568 |
(interactive) |
|
569 |
(if (use-region-p) |
|
570 |
(copy-region-as-kill nil nil 'region) |
|
571 |
(when-let ((section (magit-current-section)) |
|
572 |
(value (oref section value))) |
|
573 |
(magit-section-case |
|
574 |
((branch commit module-commit tag) |
|
575 |
(let ((default-directory default-directory) ref) |
|
576 |
(magit-section-case |
|
577 |
((branch tag) |
|
578 |
(setq ref value)) |
|
579 |
(module-commit |
|
580 |
(setq default-directory |
|
581 |
(file-name-as-directory |
|
582 |
(expand-file-name (magit-section-parent-value section) |
|
583 |
(magit-toplevel)))))) |
|
584 |
(setq value (magit-rev-parse value)) |
|
585 |
(push (list value default-directory) magit-revision-stack) |
|
586 |
(kill-new (message "%s" (or (and current-prefix-arg ref) |
|
587 |
value))))) |
|
588 |
(t (kill-new (message "%s" value))))))) |
|
589 |
|
|
590 |
;;;###autoload |
|
591 |
(defun magit-copy-buffer-revision () |
|
592 |
"Save the revision of the current buffer for later use. |
|
593 |
|
|
594 |
Save the revision shown in the current buffer to the `kill-ring' |
|
595 |
and push it to the `magit-revision-stack'. |
|
596 |
|
|
597 |
This command is mainly intended for use in `magit-revision-mode' |
|
598 |
buffers, the only buffers where it is always unambiguous exactly |
|
599 |
which revision should be saved. |
|
600 |
|
|
601 |
Most other Magit buffers usually show more than one revision, in |
|
602 |
some way or another, so this command has to select one of them, |
|
603 |
and that choice might not always be the one you think would have |
|
604 |
been the best pick. |
|
605 |
|
|
606 |
In such buffers it is often more useful to save the value of |
|
607 |
the current section instead, using `magit-copy-section-value'. |
|
608 |
|
|
609 |
When the region is active, then save that to the `kill-ring', |
|
610 |
like `kill-ring-save' would, instead of behaving as described |
|
611 |
above." |
|
612 |
(interactive) |
|
613 |
(if (use-region-p) |
|
614 |
(copy-region-as-kill nil nil 'region) |
|
615 |
(when-let ((rev (cond ((memq major-mode '(magit-cherry-mode |
|
616 |
magit-log-select-mode |
|
617 |
magit-reflog-mode |
|
618 |
magit-refs-mode |
|
619 |
magit-revision-mode |
|
620 |
magit-stash-mode |
|
621 |
magit-stashes-mode)) |
|
622 |
(car magit-refresh-args)) |
|
623 |
((memq major-mode '(magit-diff-mode |
|
624 |
magit-log-mode)) |
|
625 |
(let ((r (caar magit-refresh-args))) |
|
626 |
(if (string-match "\\.\\.\\.?\\(.+\\)" r) |
|
627 |
(match-string 1 r) |
|
628 |
r))) |
|
629 |
((eq major-mode 'magit-status-mode) "HEAD")))) |
|
630 |
(when (magit-rev-verify-commit rev) |
|
631 |
(setq rev (magit-rev-parse rev)) |
|
632 |
(push (list rev default-directory) magit-revision-stack) |
|
633 |
(kill-new (message "%s" rev)))))) |
|
634 |
|
|
635 |
;;; Miscellaneous |
|
636 |
|
|
637 |
;;;###autoload |
|
638 |
(defun magit-abort-dwim () |
|
639 |
"Abort current operation. |
|
640 |
Depending on the context, this will abort a merge, a rebase, a |
|
641 |
patch application, a cherry-pick, a revert, or a bisect." |
|
642 |
(interactive) |
|
643 |
(cond ((magit-merge-in-progress-p) (magit-merge-abort)) |
|
644 |
((magit-rebase-in-progress-p) (magit-rebase-abort)) |
|
645 |
((magit-am-in-progress-p) (magit-am-abort)) |
|
646 |
((magit-sequencer-in-progress-p) (magit-sequencer-abort)) |
|
647 |
((magit-bisect-in-progress-p) (magit-bisect-reset)))) |
|
648 |
|
|
649 |
;;; _ |
|
650 |
(provide 'magit-extras) |
|
651 |
;;; magit-extras.el ends here |