fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 1d ago
960 lines · 35.5 KB Raw Blame History
1 ;;; git-rebase.el --- Edit Git rebase files -*- lexical-binding:t -*-
2
3 ;; Copyright (C) 2008-2026 The Magit Project Contributors
4
5 ;; Author: Phil Jackson <phil@shellarchive.co.uk>
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 ;; This package assists the user in editing the list of commits to be
26 ;; rewritten during an interactive rebase.
27
28 ;; When the user initiates an interactive rebase, e.g., using "r e" in
29 ;; a Magit buffer or on the command line using "git rebase -i REV",
30 ;; Git invokes the `$GIT_SEQUENCE_EDITOR' (or if that is undefined
31 ;; `$GIT_EDITOR' or even `$EDITOR') letting the user rearrange, drop,
32 ;; reword, edit, and squash commits.
33
34 ;; This package provides the major-mode `git-rebase-mode' which makes
35 ;; doing so much more fun, by making the buffer more colorful and
36 ;; providing the following commands:
37 ;;
38 ;; C-c C-c Tell Git to make it happen.
39 ;; C-c C-k Tell Git that you changed your mind, i.e., abort.
40 ;;
41 ;; p Move point to previous line.
42 ;; n Move point to next line.
43 ;;
44 ;; M-p Move the commit at point up.
45 ;; M-n Move the commit at point down.
46 ;;
47 ;; d Drop the commit at point.
48 ;; c Keep the commit at point.
49 ;; r Change the message of the commit at point.
50 ;; e Edit the commit at point.
51 ;; s Squash the commit at point, into the one above.
52 ;; f Like "s" but don't also edit the commit message.
53 ;; b Break for editing at this point in the sequence.
54 ;; x Add a script to be run with the commit at point
55 ;; being checked out.
56 ;; k Un-/comment current line.
57 ;; z Add noop action at point.
58 ;;
59 ;; SPC Show the commit at point in another buffer.
60 ;; RET Show the commit at point in another buffer and
61 ;; select its window.
62 ;; C-/ Undo last change.
63 ;;
64 ;; Commands for --rebase-merges:
65 ;; l Associate label with current HEAD in sequence.
66 ;; MM Merge specified revisions into HEAD.
67 ;; Mt Toggle whether the merge will invoke an editor
68 ;; before committing.
69 ;; t Reset HEAD to the specified label.
70
71 ;; You should probably also read the `git-rebase' manpage.
72
73 ;;; Code:
74
75 (require 'magit)
76
77 (require 'easymenu)
78 (require 'server)
79 (require 'with-editor)
80
81 (defvar recentf-exclude)
82
83 ;;; Options
84 ;;;; Variables
85
86 (defgroup git-rebase nil
87 "Edit Git rebase sequences."
88 :link '(info-link "(magit)Editing Rebase Sequences")
89 :group 'tools)
90
91 (defcustom git-rebase-auto-advance t
92 "Whether to move to next line after changing a line."
93 :group 'git-rebase
94 :type 'boolean)
95
96 (defcustom git-rebase-show-instructions t
97 "Whether to show usage instructions inside the rebase buffer."
98 :group 'git-rebase
99 :type 'boolean)
100
101 (defcustom git-rebase-confirm-cancel t
102 "Whether confirmation is required to cancel."
103 :group 'git-rebase
104 :type 'boolean)
105
106 ;;;; Faces
107
108 (defgroup git-rebase-faces nil
109 "Faces used by Git-Rebase mode."
110 :group 'faces
111 :group 'git-rebase)
112
113 (defface git-rebase-hash '((t :inherit magit-hash))
114 "Face for commit hashes."
115 :group 'git-rebase-faces)
116
117 (defface git-rebase-label '((t :inherit magit-refname))
118 "Face for labels in label, merge, and reset lines."
119 :group 'git-rebase-faces)
120
121 (defface git-rebase-description '((t nil))
122 "Face for commit descriptions."
123 :group 'git-rebase-faces)
124
125 (defface git-rebase-action
126 '((t :inherit font-lock-keyword-face))
127 "Face for action keywords."
128 :group 'git-rebase-faces)
129
130 (defface git-rebase-killed-action
131 '((t :inherit font-lock-comment-face :strike-through t))
132 "Face for commented commit action lines."
133 :group 'git-rebase-faces)
134
135 (defface git-rebase-comment-hash
136 '((t :inherit git-rebase-hash :weight bold))
137 "Face for commit hashes in commit message comments."
138 :group 'git-rebase-faces)
139
140 (defface git-rebase-comment-heading
141 '((t :inherit font-lock-keyword-face))
142 "Face for headings in rebase message comments."
143 :group 'git-rebase-faces)
144
145 ;;; Keymaps
146
147 (defvar-keymap git-rebase-mode-map
148 :doc "Keymap for Git-Rebase mode."
149 :parent special-mode-map
150 "C-m" #'git-rebase-show-commit
151 "p" #'git-rebase-backward-line
152 "n" #'forward-line
153 "M-p" #'git-rebase-move-line-up
154 "M-n" #'git-rebase-move-line-down
155 "c" #'git-rebase-pick
156 "d" #'git-rebase-drop
157 "k" #'git-rebase-kill-line
158 "C-k" #'git-rebase-kill-line
159 "b" #'git-rebase-break
160 "e" #'git-rebase-edit
161 "l" #'git-rebase-label
162 "M M" #'git-rebase-merge
163 "M t" #'git-rebase-merge-toggle-editmsg
164 "m" #'git-rebase-edit
165 "s" #'git-rebase-squash
166 "S" #'git-rebase-squish
167 "f" #'git-rebase-fixup
168 "F" #'git-rebase-alter
169 "A" #'git-rebase-alter
170 "q" #'undefined
171 "r" #'git-rebase-reword
172 "w" #'git-rebase-reword
173 "t" #'git-rebase-reset
174 "u" #'git-rebase-update-ref
175 "x" #'git-rebase-exec
176 "y" #'git-rebase-insert
177 "z" #'git-rebase-noop
178 "SPC" #'git-rebase-show-or-scroll-up
179 "DEL" #'git-rebase-show-or-scroll-down
180 "C-x C-t" #'git-rebase-move-line-up
181 "M-<up>" #'git-rebase-move-line-up
182 "M-<down>" #'git-rebase-move-line-down
183 "<remap> <undo>" #'git-rebase-undo)
184 (put 'git-rebase-alter :advertised-binding (kbd "F"))
185 (put 'git-rebase-reword :advertised-binding (kbd "r"))
186 (put 'git-rebase-move-line-up :advertised-binding (kbd "M-p"))
187 (put 'git-rebase-kill-line :advertised-binding (kbd "k"))
188
189 (easy-menu-define git-rebase-mode-menu git-rebase-mode-map
190 "Git-Rebase mode menu."
191 '("Rebase"
192 ["Pick" git-rebase-pick t]
193 ["Drop" git-rebase-drop t]
194 ["Reword" git-rebase-reword t]
195 ["Edit" git-rebase-edit t]
196 ["Squash" git-rebase-squash t]
197 ["Fixup" git-rebase-fixup t]
198 ["Kill" git-rebase-kill-line t]
199 ["Noop" git-rebase-noop t]
200 ["Execute" git-rebase-exec t]
201 ["Move Down" git-rebase-move-line-down t]
202 ["Move Up" git-rebase-move-line-up t]
203 "---"
204 ["Cancel" with-editor-cancel t]
205 ["Finish" with-editor-finish t]))
206
207 (defvar git-rebase-command-descriptions
208 '((with-editor-finish . "tell Git to make it happen")
209 (with-editor-cancel . "tell Git that you changed your mind, i.e., abort")
210 (git-rebase-backward-line . "move point to previous line")
211 (forward-line . "move point to next line")
212 (git-rebase-move-line-up . "move the commit at point up")
213 (git-rebase-move-line-down . "move the commit at point down")
214 (git-rebase-show-or-scroll-up . "show the commit at point in another buffer")
215 (git-rebase-show-commit
216 . "show the commit at point in another buffer and select its window")
217 (undo . "undo last change")
218 (git-rebase-drop . "drop the commit at point")
219 (git-rebase-kill-line . "un-/comment current line")
220 (git-rebase-insert . "insert a line for an arbitrary commit")
221 (git-rebase-noop . "add noop action at point")))
222
223 (defvar git-rebase-fixup-descriptions
224 '((git-rebase-squish
225 . "fixup -c <commit> = use commit, but meld into previous commit,\n#\
226 dropping previous commit's message, and open the editor")
227 (git-rebase-fixup
228 . "fixup <commit> = use commit, but meld into previous commit,\n#\
229 dropping <commit>'s message")
230 (git-rebase-alter
231 . "fixup -C <commit> = use commit, but meld into previous commit,\n#\
232 dropping previous commit's message")))
233
234 ;;; Commands
235
236 (defun git-rebase-pick ()
237 "Use commit on current line.
238 If the region is active, act on all lines touched by the region."
239 (interactive)
240 (git-rebase-set-action "pick"))
241
242 (defun git-rebase-drop ()
243 "Drop commit on current line.
244 If the region is active, act on all lines touched by the region."
245 (interactive)
246 (git-rebase-set-action "drop"))
247
248 (defun git-rebase-reword ()
249 "Edit message of commit on current line.
250 If the region is active, act on all lines touched by the region."
251 (interactive)
252 (git-rebase-set-action "reword"))
253
254 (defun git-rebase-edit ()
255 "Stop at the commit on the current line.
256 If the region is active, act on all lines touched by the region."
257 (interactive)
258 (git-rebase-set-action "edit"))
259
260 (defun git-rebase-squash ()
261 "Fold commit on current line into previous commit, edit combined message.
262 If the region is active, act on all lines touched by the region."
263 (interactive)
264 (git-rebase-set-action "squash"))
265
266 (defun git-rebase-squish ()
267 "Fold current into previous commit, discard previous message and edit current.
268 This is like `git-rebase-squash', except that the other message is kept.
269 The action indicatore shown in the list commits is \"fixup -c\". If the
270 region is active, act on all lines touched by the region."
271 (interactive)
272 (git-rebase-set-action "fixup -c"))
273
274 (defun git-rebase-fixup ()
275 "Fold commit on current line into previous commit, discard current message.
276 If the region is active, act on all lines touched by the region."
277 (interactive)
278 (git-rebase-set-action "fixup"))
279
280 (defun git-rebase-alter ()
281 "Meld current into previous commit, discard previous message and use current.
282 This is like `git-rebase-fixup', except that the other message is kept.
283 The action indicatore shown in the list commits is \"fixup -C\". If the
284 region is active, act on all lines touched by the region."
285 (interactive)
286 (git-rebase-set-action "fixup -C"))
287
288 (defvar-local git-rebase-comment-re nil)
289
290 (defvar git-rebase-short-options
291 '((?b . "break")
292 (?d . "drop")
293 (?e . "edit")
294 (?f . "fixup")
295 (?l . "label")
296 (?m . "merge")
297 (?p . "pick")
298 (?r . "reword")
299 (?s . "squash")
300 (?t . "reset")
301 (?u . "update-ref")
302 (?x . "exec"))
303 "Alist mapping single key of an action to the full name.")
304
305 (defclass git-rebase-action ()
306 (;; action-type: commit, exec, bare, label, merge
307 (action-type :initarg :action-type :initform nil)
308 ;; Examples for each action type:
309 ;; | action | action options | target | trailer |
310 ;; |--------+----------------+---------+---------|
311 ;; | pick | | hash | subject |
312 ;; | exec | | command | |
313 ;; | noop | | | |
314 ;; | reset | | name | subject |
315 ;; | merge | -C hash | name | subject |
316 (action :initarg :action :initform nil)
317 (action-options :initarg :action-options :initform nil)
318 (target :initarg :target :initform nil)
319 (trailer :initarg :trailer :initform nil)
320 (comment-p :initarg :comment-p :initform nil)
321 (abbrev)))
322
323 (defvar git-rebase-line-regexps
324 ;; 1: action, 2: option, 3: target, 4: "#", 5: description.
325 ;;
326 ;; <action> <commit> [[# ] <oneline>]
327 ;; fixup [-C|-c] <commit> [[# ] <oneline>]
328 `((commit . ,(concat
329 (regexp-opt '("d" "drop"
330 "e" "edit"
331 "f" "fixup"
332 "f -C" "fixup -C"
333 "f -c" "fixup -c"
334 "p" "pick"
335 "r" "reword"
336 "s" "squash")
337 "\\(?1:")
338 " \\(?3:[^ \n]+\\)"
339 "\\(?: \\(?4:# \\)?\\(?5:.*\\)\\)?"))
340 (exec . "\\(?1:x\\|exec\\) \\(?3:.*\\)")
341 (bare . ,(concat (regexp-opt '("b" "break" "noop") "\\(?1:")
342 " *$"))
343 (label . ,(concat (regexp-opt '("l" "label"
344 "t" "reset"
345 "u" "update-ref")
346 "\\(?1:")
347 " \\(?3:[^ \n]+\\)"
348 "\\(?: \\(?4:# \\)?\\(?5:.*\\)\\)?"))
349 ;; merge [-C <commit> | -c <commit>] <label> [# <oneline>]
350 ;; <commit> is matched by group 22 (part of group 2), not group 3
351 (merge . ,(concat "\\(?1:m\\|merge\\) "
352 "\\(?:\\(?2:\\(?21:-[cC]\\) \\(?22:[^ \n]+\\)\\) \\)?"
353 "\\(?3:[^ \n]+\\)"
354 "\\(?: \\(?4:# \\)?\\(?5:.*\\)\\)?"))))
355
356 ;;;###autoload
357 (defun git-rebase-current-line (&optional batch)
358 "Parse current line into a `git-rebase-action' instance.
359 If the current line isn't recognized as a rebase line, an
360 instance with all nil values is returned, unless optional
361 BATCH is non-nil, in which case nil is returned. Non-nil
362 BATCH also ignores commented lines."
363 (save-excursion
364 (goto-char (line-beginning-position))
365 (cond-let*
366 ([re-start (if batch
367 "^"
368 (format "^\\(?99:%s\\)? *" (regexp-quote comment-start)))]
369 [type (seq-some (pcase-lambda (`(,type . ,re))
370 (let ((case-fold-search nil))
371 (and (looking-at (concat re-start re)) type)))
372 git-rebase-line-regexps)]
373 (git-rebase-action
374 :action-type type
375 :action (and-let ((action (match-str 1)))
376 (or (cdr (assoc action git-rebase-short-options))
377 action))
378 :action-options (match-str 2)
379 :target (match-str 3)
380 :trailer (match-str 5)
381 :comment-p (and (match-str 99) t)))
382 ((not batch)
383 ;; Use empty object rather than nil to ease handling.
384 (git-rebase-action)))))
385
386 (defun git-rebase-set-action (action)
387 "Set action of commit line to ACTION.
388 If the region is active, operate on all lines that it touches.
389 Otherwise, operate on the current line. As a special case, an
390 ACTION of nil comments or uncomments the rebase line, regardless
391 of its action type."
392 (pcase (git-rebase-region-bounds t)
393 (`(,beg ,end)
394 (let ((end-marker (copy-marker end))
395 (pt-below-p (and mark-active (< (mark) (point)))))
396 (set-marker-insertion-type end-marker t)
397 (goto-char beg)
398 (while (< (point) end-marker)
399 (with-slots (action-type target trailer comment-p)
400 (git-rebase-current-line)
401 (cond
402 ((and action (eq action-type 'commit))
403 (let ((inhibit-read-only t))
404 (magit-delete-line)
405 (insert (concat action " " target " "))
406 (when (magit-git-version>= "2.50.0")
407 (insert "# "))
408 (insert (concat trailer "\n"))))
409 ((and (not action) action-type)
410 (let ((inhibit-read-only t))
411 (if comment-p
412 (delete-region beg (+ beg 2))
413 (insert comment-start " ")))
414 (forward-line))
415 ;; In the case of --rebase-merges, commit lines may have
416 ;; other lines with other action types, empty lines, and
417 ;; "Branch" comments interspersed. Move along.
418 ((forward-line)))))
419 (goto-char (cond (git-rebase-auto-advance end-marker)
420 (pt-below-p (1- end-marker))
421 (beg)))
422 (goto-char (line-beginning-position))))
423 (_ (ding))))
424
425 (defun git-rebase-line-p (&optional pos)
426 (save-excursion
427 (when pos (goto-char pos))
428 (and (oref (git-rebase-current-line) action-type)
429 t)))
430
431 (defun git-rebase-region-bounds (&optional fallback)
432 "Return region bounds if both ends touch rebase lines.
433 Each bound is extended to include the entire line touched by the
434 point or mark. If the region isn't active and FALLBACK is
435 non-nil, return the beginning and end of the current rebase line,
436 if any."
437 (cond
438 ((use-region-p)
439 (let ((beg (magit--bol-position (region-beginning)))
440 (end (magit--eol-position (region-end))))
441 (and (git-rebase-line-p beg)
442 (git-rebase-line-p end)
443 (list beg (1+ end)))))
444 ((and fallback (git-rebase-line-p))
445 (list (line-beginning-position)
446 (1+ (line-end-position))))))
447
448 (defun git-rebase-move-line-down (n)
449 "Move the current commit (or command) N lines down.
450 If N is negative, move the commit up instead. With an active
451 region, move all the lines that the region touches, not just the
452 current line."
453 (interactive "p")
454 (pcase-let* ((`(,beg ,end)
455 (or (git-rebase-region-bounds)
456 (list (line-beginning-position)
457 (1+ (line-end-position)))))
458 (pt-offset (- (point) beg))
459 (mark-offset (and mark-active (- (mark) beg))))
460 (save-restriction
461 (narrow-to-region
462 (point-min)
463 (1-
464 (if git-rebase-show-instructions
465 (save-excursion
466 (goto-char (point-min))
467 (while (or (git-rebase-line-p)
468 ;; The output for --rebase-merges has empty
469 ;; lines and "Branch" comments interspersed.
470 (looking-at-p "^$")
471 (looking-at-p (concat git-rebase-comment-re
472 " Branch")))
473 (forward-line))
474 (line-beginning-position))
475 (point-max))))
476 (if (or (and (< n 0) (= beg (point-min)))
477 (and (> n 0) (= end (point-max)))
478 (> end (point-max)))
479 (ding)
480 (goto-char (if (< n 0) beg end))
481 (forward-line n)
482 (atomic-change-group
483 (let ((inhibit-read-only t))
484 (insert (delete-and-extract-region beg end)))
485 (let ((new-beg (- (point) (- end beg))))
486 (when (use-region-p)
487 (setq deactivate-mark nil)
488 (set-mark (+ new-beg mark-offset)))
489 (goto-char (+ new-beg pt-offset))))))))
490
491 (defun git-rebase-move-line-up (n)
492 "Move the current commit (or command) N lines up.
493 If N is negative, move the commit down instead. With an active
494 region, move all the lines that the region touches, not just the
495 current line."
496 (interactive "p")
497 (git-rebase-move-line-down (- n)))
498
499 (defun git-rebase-highlight-region (start end window rol)
500 (let ((inhibit-read-only t)
501 (deactivate-mark nil)
502 (bounds (git-rebase-region-bounds)))
503 (mapc #'delete-overlay magit-section-highlight-overlays)
504 (when bounds
505 (magit-section-highlight-range (car bounds) (cadr bounds)
506 'magit-section-heading-selection))
507 (if (and bounds (not magit-section-keep-region-overlay))
508 (funcall (default-value 'redisplay-unhighlight-region-function) rol)
509 (funcall (default-value 'redisplay-highlight-region-function)
510 start end window rol))))
511
512 (defun git-rebase-unhighlight-region (rol)
513 (mapc #'delete-overlay magit-section-highlight-overlays)
514 (funcall (default-value 'redisplay-unhighlight-region-function) rol))
515
516 (defun git-rebase-kill-line ()
517 "Comment the current action line.
518 If the action line is already commented, then uncomment it.
519 If the region is active, act on all lines touched by the region."
520 (interactive)
521 (git-rebase-set-action nil))
522
523 (defun git-rebase-insert (rev)
524 "Read an arbitrary commit and insert it below current line."
525 (interactive (list (magit-read-branch-or-commit "Insert revision")))
526 (forward-line)
527 (if-let ((info (magit-rev-format "%h %s" rev)))
528 (let ((inhibit-read-only t))
529 (insert "pick " info ?\n))
530 (user-error "Unknown revision")))
531
532 (defun git-rebase-set-noncommit-action (action value-fn arg)
533 (goto-char (line-beginning-position))
534 (pcase-let* ((inhibit-read-only t)
535 (`(,initial ,trailer ,comment-p)
536 (and (not arg)
537 (with-slots ((ln-action action)
538 target trailer comment-p)
539 (git-rebase-current-line)
540 (and (equal ln-action action)
541 (list target trailer comment-p)))))
542 (value (funcall value-fn initial)))
543 (pcase (list value initial comment-p)
544 (`("" nil ,_)
545 (ding))
546 (`("" ,_ ,_)
547 (magit-delete-line))
548 (_
549 (if initial
550 (magit-delete-line)
551 (forward-line))
552 (insert (concat action " " value
553 (and (equal value initial)
554 trailer
555 (concat " " trailer))
556 "\n"))
557 (unless git-rebase-auto-advance
558 (forward-line -1))))))
559
560 (defun git-rebase-exec (arg)
561 "Insert a shell command to be run after the current commit.
562
563 If there already is such a command on the current line, then edit
564 that instead. With a prefix argument insert a new command even
565 when there already is one on the current line. With empty input
566 remove the command on the current line, if any."
567 (interactive "P")
568 (git-rebase-set-noncommit-action
569 "exec"
570 (lambda (initial) (read-shell-command "Execute: " initial))
571 arg))
572
573 (defun git-rebase-label (arg)
574 "Add a label after the current commit.
575 If there already is a label on the current line, then edit that
576 instead. With a prefix argument, insert a new label even when
577 there is already a label on the current line. With empty input,
578 remove the label on the current line, if any."
579 (interactive "P")
580 (git-rebase-set-noncommit-action
581 "label"
582 (lambda (initial)
583 (read-from-minibuffer
584 "Label: " initial magit-minibuffer-local-ns-map))
585 arg))
586
587 (defun git-rebase-buffer-labels ()
588 (let (labels)
589 (save-excursion
590 (goto-char (point-min))
591 (while (re-search-forward "^\\(?:l\\|label\\) \\([^ \n]+\\)" nil t)
592 (push (match-str 1) labels)))
593 (nreverse labels)))
594
595 (defun git-rebase-reset (arg)
596 "Reset the current HEAD to a label.
597 If there already is a reset command on the current line, then
598 edit that instead. With a prefix argument, insert a new reset
599 line even when point is already on a reset line. With empty
600 input, remove the reset command on the current line, if any."
601 (interactive "P")
602 (git-rebase-set-noncommit-action
603 "reset"
604 (lambda (initial)
605 (or (magit-completing-read "Label" (git-rebase-buffer-labels)
606 nil t initial)
607 ""))
608 arg))
609
610 (defun git-rebase-update-ref (arg)
611 "Insert an update-ref action after the current line.
612 If there is already an update-ref action on the current line,
613 then edit that instead. With a prefix argument, insert a new
614 action even when there is already one on the current line. With
615 empty input, remove the action on the current line, if any."
616 (interactive "P")
617 (git-rebase-set-noncommit-action
618 "update-ref"
619 (lambda (initial)
620 (or (magit-completing-read "Ref" (magit-list-refs) nil nil initial)
621 ""))
622 arg))
623
624 (defun git-rebase-merge (arg)
625 "Add a merge command after the current commit.
626 If there is already a merge command on the current line, then
627 replace that command instead. With a prefix argument, insert a
628 new merge command even when there is already one on the current
629 line. With empty input, remove the merge command on the current
630 line, if any."
631 (interactive "P")
632 (git-rebase-set-noncommit-action
633 "merge"
634 (lambda (_)
635 (or (magit-completing-read "Merge" (git-rebase-buffer-labels))
636 ""))
637 arg))
638
639 (defun git-rebase-merge-toggle-editmsg ()
640 "Toggle whether an editor is invoked when performing the merge at point.
641 When a merge command uses a lower-case -c, the message for the
642 specified commit will be opened in an editor before creating the
643 commit. For an upper-case -C, the message will be used as is."
644 (interactive)
645 (with-slots (action-type target action-options trailer)
646 (git-rebase-current-line)
647 (if (eq action-type 'merge)
648 (let ((inhibit-read-only t))
649 (magit-delete-line)
650 (insert
651 (format "merge %s %s %s\n"
652 (replace-regexp-in-string
653 "-[cC]" (##if (equal % "-c") "-C" "-c")
654 action-options t t)
655 target
656 trailer)))
657 (ding))))
658
659 (defun git-rebase-set-bare-action (action arg)
660 (goto-char (line-beginning-position))
661 (with-slots ((ln-action action) comment-p)
662 (git-rebase-current-line)
663 (let ((same-action-p (equal action ln-action))
664 (inhibit-read-only t))
665 (when (or arg
666 (not ln-action)
667 (not same-action-p)
668 (and same-action-p comment-p))
669 (unless (or arg (not same-action-p))
670 (magit-delete-line))
671 (insert action ?\n)
672 (unless git-rebase-auto-advance
673 (forward-line -1))))))
674
675 (defun git-rebase-noop (&optional arg)
676 "Add noop action at point.
677
678 If the current line already contains a noop action, leave it
679 unchanged. If there is a commented noop action present, remove
680 the comment. Otherwise add a new noop action. With a prefix
681 argument insert a new noop action regardless of what is already
682 present on the current line.
683
684 A noop action can be used to make git perform a rebase even if
685 no commits are selected. Without the noop action present, git
686 would see an empty file and therefore do nothing."
687 (interactive "P")
688 (git-rebase-set-bare-action "noop" arg))
689
690 (defun git-rebase-break (&optional arg)
691 "Add break action at point.
692
693 If there is a commented break action present, remove the comment.
694 If the current line already contains a break action, add another
695 break action only if a prefix argument is given.
696
697 A break action can be used to interrupt the rebase at the
698 specified point. It is particularly useful for pausing before
699 the first commit in the sequence. For other cases, the
700 equivalent behavior can be achieved with `git-rebase-edit'."
701 (interactive "P")
702 (git-rebase-set-bare-action "break" arg))
703
704 (defun git-rebase-undo (&optional arg)
705 "Undo some previous changes.
706 Like `undo' but works in read-only buffers."
707 (interactive "P")
708 (let ((inhibit-read-only t))
709 (undo arg)))
710
711 (defun git-rebase--show-commit (&optional scroll)
712 (let ((magit--disable-save-buffers t))
713 (save-excursion
714 (goto-char (line-beginning-position))
715 (if-let ((rev (with-slots (action-type target)
716 (git-rebase-current-line)
717 (and (eq action-type 'commit)
718 target))))
719 (pcase scroll
720 ('up (magit-diff-show-or-scroll-up))
721 ('down (magit-diff-show-or-scroll-down))
722 (_ (apply #'magit-show-commit rev
723 (magit-diff-arguments 'magit-revision-mode))))
724 (ding)))))
725
726 (defun git-rebase-show-commit ()
727 "Show the commit on the current line if any."
728 (interactive)
729 (git-rebase--show-commit))
730
731 (defun git-rebase-show-or-scroll-up ()
732 "Update the commit buffer for commit on current line.
733
734 Either show the commit at point in the appropriate buffer, or if
735 that buffer is already being displayed in the current frame and
736 contains information about that commit, then instead scroll the
737 buffer up."
738 (interactive)
739 (git-rebase--show-commit 'up))
740
741 (defun git-rebase-show-or-scroll-down ()
742 "Update the commit buffer for commit on current line.
743
744 Either show the commit at point in the appropriate buffer, or if
745 that buffer is already being displayed in the current frame and
746 contains information about that commit, then instead scroll the
747 buffer down."
748 (interactive)
749 (git-rebase--show-commit 'down))
750
751 (defun git-rebase-backward-line (&optional n)
752 "Move N lines backward (forward if N is negative).
753 Like `forward-line' but go into the opposite direction."
754 (interactive "p")
755 (forward-line (- (or n 1))))
756
757 ;;; Mode
758
759 ;;;###autoload
760 (define-derived-mode git-rebase-mode special-mode "Git Rebase"
761 "Major mode for editing of a Git rebase file.
762
763 Rebase files are generated when you run \"git rebase -i\" or run
764 `magit-interactive-rebase'. They describe how Git should perform
765 the rebase. See the documentation for git-rebase (e.g., by
766 running \"man git-rebase\" at the command line) for details."
767 :interactive nil
768 :group 'git-rebase
769 (setq comment-start (or (magit-get "core.commentChar") "#"))
770 (setq git-rebase-comment-re (concat "^" (regexp-quote comment-start)))
771 (setq font-lock-defaults (list (git-rebase-mode-font-lock-keywords) t t))
772 (unless git-rebase-show-instructions
773 (let ((inhibit-read-only t))
774 (flush-lines git-rebase-comment-re)))
775 (unless with-editor-mode
776 ;; Maybe already enabled when using `shell-command' or an Emacs shell.
777 (with-editor-mode 1))
778 (when git-rebase-confirm-cancel
779 (add-hook 'with-editor-cancel-query-functions
780 #'git-rebase-cancel-confirm nil t))
781 (setq-local redisplay-highlight-region-function
782 #'git-rebase-highlight-region)
783 (setq-local redisplay-unhighlight-region-function
784 #'git-rebase-unhighlight-region)
785 (add-hook 'with-editor-pre-cancel-hook #'git-rebase-autostash-save nil t)
786 (add-hook 'with-editor-post-cancel-hook #'git-rebase-autostash-apply nil t)
787 (setq imenu-prev-index-position-function
788 #'magit-imenu--rebase-prev-index-position-function)
789 (setq imenu-extract-index-name-function
790 #'magit-imenu--rebase-extract-index-name-function)
791 (when (boundp 'save-place)
792 (setq save-place nil)))
793
794 (defun git-rebase-cancel-confirm (force)
795 (or (not (buffer-modified-p))
796 force
797 (magit-confirm 'abort-rebase "Abort this rebase" nil 'noabort)))
798
799 (defun git-rebase-autostash-save ()
800 (when-let ((rev (magit-file-line
801 (expand-file-name "rebase-merge/autostash" (magit-gitdir)))))
802 (push (cons 'stash rev) with-editor-cancel-alist)))
803
804 (defun git-rebase-autostash-apply ()
805 (when-let ((rev (cdr (assq 'stash with-editor-cancel-alist))))
806 (magit-stash-apply rev)))
807
808 (defun git-rebase-match-comment-line (limit)
809 (re-search-forward (concat git-rebase-comment-re ".*") limit t))
810
811 (defun git-rebase-mode-font-lock-keywords ()
812 "Font lock keywords for Git-Rebase mode."
813 `((,(concat "^" (cdr (assq 'commit git-rebase-line-regexps)))
814 (1 'git-rebase-action)
815 (3 'git-rebase-hash)
816 (4 'font-lock-comment-face nil t)
817 (5 'git-rebase-description nil t))
818 (,(concat "^" (cdr (assq 'exec git-rebase-line-regexps)))
819 (1 'git-rebase-action)
820 (3 'git-rebase-description))
821 (,(concat "^" (cdr (assq 'bare git-rebase-line-regexps)))
822 (1 'git-rebase-action))
823 (,(concat "^" (cdr (assq 'label git-rebase-line-regexps)))
824 (1 'git-rebase-action)
825 (3 'git-rebase-label)
826 (4 'font-lock-comment-face nil t)
827 (5 'git-rebase-description nil t))
828 (,(concat "^" (cdr (assq 'merge git-rebase-line-regexps)))
829 (1 'git-rebase-action)
830 (21 'git-rebase-action nil t)
831 (22 'git-rebase-hash t t)
832 (3 'magit-branch-local)
833 (4 'font-lock-comment-face nil t)
834 (5 'git-rebase-description nil t))
835 (,(format "^%s Branch \\(.*\\)" comment-start)
836 (1 'magit-branch-local t))
837 ("^drop \\(.+\\)"
838 (1 'git-rebase-killed-action t))
839 (,(concat git-rebase-comment-re " *"
840 (cdr (assq 'commit git-rebase-line-regexps)))
841 (0 'git-rebase-killed-action t))
842 (git-rebase-match-comment-line
843 (0 'font-lock-comment-face))
844 ("\\[[^[]*\\]"
845 (0 'magit-keyword t))
846 ("\\(?:fixup!\\|squash!\\|amend!\\)"
847 (0 'magit-keyword-squash t))
848 (,(format "^%s Rebase \\([^ ]*\\) onto \\([^ ]*\\)" comment-start)
849 (1 'git-rebase-comment-hash t)
850 (2 'git-rebase-comment-hash t))
851 (,(format "^%s \\(Commands:\\)" comment-start)
852 (1 'git-rebase-comment-heading t))))
853
854 (defun git-rebase-mode-show-keybindings ()
855 "Modify the \"Commands:\" section of the comment Git generates.
856 Modify that section to replace Git's one-letter command abbreviation,
857 with the key bindings used in Magit. By default, these are the same,
858 except for the \"pick\" command."
859 (let ((inhibit-read-only t))
860 (save-excursion
861 (goto-char (point-min))
862 (when (and git-rebase-show-instructions
863 (re-search-forward
864 (concat git-rebase-comment-re "\\s-+p, pick")
865 nil t))
866 (goto-char (line-beginning-position))
867 (git-rebase--insert-descriptions git-rebase-command-descriptions)
868 (let ((cmd nil)
869 (line (concat git-rebase-comment-re "\\(?:\\( \\.? *\\)\\|"
870 "\\( +\\)\\([^\n,],\\) \\([^\n ]+\\) \\)")))
871 (while (re-search-forward line nil t)
872 (if (match-str 1)
873 (if (assq cmd git-rebase-fixup-descriptions)
874 (delete-line)
875 (replace-match (make-string 10 ?\s) t t nil 1))
876 (setq cmd (intern (concat "git-rebase-" (match-str 4))))
877 (cond
878 ((not (fboundp cmd))
879 (delete-line))
880 ((eq cmd 'git-rebase-fixup)
881 (delete-line)
882 (git-rebase--insert-descriptions git-rebase-fixup-descriptions))
883 (t
884 (add-text-properties (line-beginning-position)
885 (1+ (line-end-position))
886 '(font-lock-face font-lock-comment-face))
887 (replace-match " " t t nil 2)
888 (replace-match
889 (string-pad
890 (save-match-data
891 (substitute-command-keys (format "\\[%s]" cmd)))
892 8)
893 t t nil 3))))))))))
894
895 (defun git-rebase--insert-descriptions (alist)
896 (pcase-dolist (`(,cmd . ,desc) alist)
897 (insert
898 (format (propertize "%s %s %s\n" 'font-lock-face 'font-lock-comment-face)
899 comment-start
900 (string-pad (substitute-command-keys (format "\\[%s]" cmd)) 8)
901 (string-replace "#" comment-start desc)))))
902
903 (add-hook 'git-rebase-mode-hook #'git-rebase-mode-show-keybindings t)
904
905 (defun git-rebase-mode-disable-before-save-hook ()
906 (setq-local before-save-hook nil))
907
908 (add-hook 'git-rebase-mode-hook #'git-rebase-mode-disable-before-save-hook)
909
910 ;;;###autoload
911 (defconst git-rebase-filename-regexp "/git-rebase-todo\\'")
912 ;;;###autoload
913 (add-to-list 'auto-mode-alist
914 (cons git-rebase-filename-regexp #'git-rebase-mode))
915
916 (add-to-list 'with-editor-server-window-alist
917 (cons git-rebase-filename-regexp #'switch-to-buffer))
918
919 (with-eval-after-load 'recentf
920 (add-to-list 'recentf-exclude git-rebase-filename-regexp))
921
922 (add-to-list 'with-editor-file-name-history-exclude git-rebase-filename-regexp)
923
924 ;;; Imenu Support
925
926 (defun magit-imenu--rebase-prev-index-position-function ()
927 "Move point to previous commit in git-rebase buffer.
928 Used as a value for `imenu-prev-index-position-function'."
929 (catch 'found
930 (while (not (bobp))
931 (git-rebase-backward-line)
932 (when (git-rebase-line-p)
933 (throw 'found t)))))
934
935 (defun magit-imenu--rebase-extract-index-name-function ()
936 "Return imenu name for line at point.
937 Point should be at the beginning of the line. This function
938 is used as a value for `imenu-extract-index-name-function'."
939 (buffer-substring-no-properties (line-beginning-position)
940 (line-end-position)))
941
942 ;;; _
943 (provide 'git-rebase)
944 ;; Local Variables:
945 ;; read-symbol-shorthands: (
946 ;; ("and$" . "cond-let--and$")
947 ;; ("thread$" . "cond-let--thread$")
948 ;; ("when$" . "cond-let--when$")
949 ;; ("and-let*" . "cond-let--and-let*")
950 ;; ("and-let" . "cond-let--and-let")
951 ;; ("if-let*" . "cond-let--if-let*")
952 ;; ("if-let" . "cond-let--if-let")
953 ;; ("when-let*" . "cond-let--when-let*")
954 ;; ("when-let" . "cond-let--when-let")
955 ;; ("while-let*" . "cond-let--while-let*")
956 ;; ("while-let" . "cond-let--while-let")
957 ;; ("match-string" . "match-string")
958 ;; ("match-str" . "match-string-no-properties"))
959 ;; End:
960 ;;; git-rebase.el ends here