mirror of https://github.com/Chizi123/.emacs.d.git

Chizi123
2018-11-18 21067e7cbe6d7a0f65ff5c317a96b5c337b0b3d8
commit | author | age
5cb5f7 1 ;;; magit-sequence.el --- history manipulation in Magit  -*- lexical-binding: t -*-
C 2
3 ;; Copyright (C) 2011-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 ;; Author: Jonas Bernoulli <jonas@bernoul.li>
9 ;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
10
11 ;; Magit is free software; you can redistribute it and/or modify it
12 ;; under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation; either version 3, or (at your option)
14 ;; any later version.
15 ;;
16 ;; Magit is distributed in the hope that it will be useful, but WITHOUT
17 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
18 ;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
19 ;; License for more details.
20 ;;
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with Magit.  If not, see http://www.gnu.org/licenses.
23
24 ;;; Commentary:
25
26 ;; Support for Git commands that replay commits and help the user make
27 ;; changes along the way.  Supports `cherry-pick', `revert', `rebase',
28 ;; `rebase--interactive' and `am'.
29
30 ;;; Code:
31
32 (eval-when-compile
33   (require 'subr-x))
34
35 (require 'magit)
36
37 ;;; Options
38 ;;;; Faces
39
40 (defface magit-sequence-pick
41   '((t :inherit default))
42   "Face used in sequence sections."
43   :group 'magit-faces)
44
45 (defface magit-sequence-stop
46   '((((class color) (background light)) :foreground "DarkOliveGreen4")
47     (((class color) (background dark))  :foreground "DarkSeaGreen2"))
48   "Face used in sequence sections."
49   :group 'magit-faces)
50
51 (defface magit-sequence-part
52   '((((class color) (background light)) :foreground "Goldenrod4")
53     (((class color) (background dark))  :foreground "LightGoldenrod2"))
54   "Face used in sequence sections."
55   :group 'magit-faces)
56
57 (defface magit-sequence-head
58   '((((class color) (background light)) :foreground "SkyBlue4")
59     (((class color) (background dark))  :foreground "LightSkyBlue1"))
60   "Face used in sequence sections."
61   :group 'magit-faces)
62
63 (defface magit-sequence-drop
64   '((((class color) (background light)) :foreground "IndianRed")
65     (((class color) (background dark))  :foreground "IndianRed"))
66   "Face used in sequence sections."
67   :group 'magit-faces)
68
69 (defface magit-sequence-done
70   '((t :inherit magit-hash))
71   "Face used in sequence sections."
72   :group 'magit-faces)
73
74 (defface magit-sequence-onto
75   '((t :inherit magit-sequence-done))
76   "Face used in sequence sections."
77   :group 'magit-faces)
78
79 (defface magit-sequence-exec
80   '((t :inherit magit-hash))
81   "Face used in sequence sections."
82   :group 'magit-faces)
83
84 ;;; Common
85
86 ;;;###autoload
87 (defun magit-sequencer-continue ()
88   "Resume the current cherry-pick or revert sequence."
89   (interactive)
90   (if (magit-sequencer-in-progress-p)
91       (if (magit-anything-unstaged-p t)
92           (user-error "Cannot continue due to unstaged changes")
93         (magit-run-git-sequencer
94          (if (magit-revert-in-progress-p) "revert" "cherry-pick") "--continue"))
95     (user-error "No cherry-pick or revert in progress")))
96
97 ;;;###autoload
98 (defun magit-sequencer-skip ()
99   "Skip the stopped at commit during a cherry-pick or revert sequence."
100   (interactive)
101   (if (magit-sequencer-in-progress-p)
102       (progn (magit-call-git "reset" "--hard")
103              (magit-sequencer-continue))
104     (user-error "No cherry-pick or revert in progress")))
105
106 ;;;###autoload
107 (defun magit-sequencer-abort ()
108   "Abort the current cherry-pick or revert sequence.
109 This discards all changes made since the sequence started."
110   (interactive)
111   (if (magit-sequencer-in-progress-p)
112       (magit-run-git-sequencer
113        (if (magit-revert-in-progress-p) "revert" "cherry-pick") "--abort")
114     (user-error "No cherry-pick or revert in progress")))
115
116 (defun magit-sequencer-in-progress-p ()
117   (or (magit-cherry-pick-in-progress-p)
118       (magit-revert-in-progress-p)))
119
120 ;;; Cherry-Pick
121
122 (defvar magit-perl-executable "perl"
123   "The Perl executable.")
124
125 ;;;###autoload (autoload 'magit-cherry-pick-popup "magit-sequence" nil t)
126 (magit-define-popup magit-cherry-pick-popup
127   "Popup console for cherry-pick commands."
128   :man-page "git-cherry-pick"
129   :switches '((?s "Add Signed-off-by lines"            "--signoff")
130               (?e "Edit commit messages"               "--edit")
131               (?x "Reference cherry in commit message" "-x")
132               (?F "Attempt fast-forward"               "--ff"))
133   :options  '((?s "Strategy"                        "--strategy=")
134               (?m "Replay merge relative to parent" "--mainline="))
135   :actions  '("Apply here"
136               (?A "Pick"    magit-cherry-copy)
137               (?a "Apply"   magit-cherry-apply)
138               (?h "Harvest" magit-cherry-harvest)
139               "Apply elsewhere"
140               (?d "Donate"  magit-cherry-donate)
141               (?n "Spinout" magit-cherry-spinout)
142               (?s "Spinoff" magit-cherry-spinoff))
143   :sequence-actions '((?A "Continue" magit-sequencer-continue)
144                       (?s "Skip"     magit-sequencer-skip)
145                       (?a "Abort"    magit-sequencer-abort))
146   :sequence-predicate 'magit-sequencer-in-progress-p
147   :default-arguments '("--ff"))
148
149 (defun magit-cherry-pick-read-args (prompt)
150   (list (or (nreverse (magit-region-values 'commit))
151             (magit-read-other-branch-or-commit prompt))
152         (magit-cherry-pick-arguments)))
153
154 (defun magit--cherry-move-read-args (verb away fn)
155   (declare (indent defun))
156    (let ((commits (or (nreverse (magit-region-values 'commit))
157                       (list (funcall (if away
158                                          'magit-read-branch-or-commit
159                                        'magit-read-other-branch-or-commit)
160                                      (format "%s cherry" (capitalize verb))))))
161          (current (magit-get-current-branch)))
162      (unless current
163        (user-error "Cannot %s cherries while HEAD is detached" verb))
164      (let ((reachable (magit-rev-ancestor-p (car commits) current))
165            (msg "Cannot %s cherries that %s reachable from HEAD"))
166        (pcase (list away reachable)
167          (`(nil t) (user-error msg verb "are"))
168          (`(t nil) (user-error msg verb "are not"))))
169      `(,commits
170        ,@(funcall fn commits)
171        ,(magit-cherry-pick-arguments))))
172
173 (defun magit--cherry-spinoff-read-args (verb)
174   (magit--cherry-move-read-args verb t
175     (lambda (commits)
176       (butlast (magit-branch-read-args
177                 (format "Create branch from %s cherries" commits))))))
178
179 ;;;###autoload
180 (defun magit-cherry-copy (commits &optional args)
181   "Copy COMMITS from another branch onto the current branch.
182 Prompt for a commit, defaulting to the commit at point.  If
183 the region selects multiple commits, then pick all of them,
184 without prompting."
185   (interactive (magit-cherry-pick-read-args "Cherry-pick"))
186   (magit--cherry-pick commits args))
187
188 ;;;###autoload
189 (defun magit-cherry-apply (commits &optional args)
190   "Apply the changes in COMMITS but do not commit them.
191 Prompt for a commit, defaulting to the commit at point.  If
192 the region selects multiple commits, then apply all of them,
193 without prompting."
194   (interactive (magit-cherry-pick-read-args "Apply changes from commit"))
195   (magit--cherry-pick commits (cons "--no-commit" (remove "--ff" args))))
196
197 ;;;###autoload
198 (defun magit-cherry-harvest (commits branch &optional args)
199   "Move COMMITS from another BRANCH onto the current branch.
200 Remove the COMMITS from BRANCH and stay on the current branch.
201 If a conflict occurs, then you have to fix that and finish the
202 process manually."
203   (interactive
204    (magit--cherry-move-read-args "harvest" nil
205      (lambda (commits)
206        (list (let ((branches (magit-list-containing-branches (car commits))))
207                (pcase (length branches)
208                  (0 nil)
209                  (1 (car branches))
210                  (_ (magit-completing-read
211                      (format "Remove %s cherries from branch" (length commits))
212                      branches nil t))))))))
213   (magit--cherry-move commits branch (magit-get-current-branch) args nil t))
214
215 ;;;###autoload
216 (defun magit-cherry-donate (commits branch &optional args)
217   "Move COMMITS from the current branch onto another existing BRANCH.
218 Remove COMMITS from the current branch and stay on that branch.
219 If a conflict occurs, then you have to fix that and finish the
220 process manually."
221   (interactive
222    (magit--cherry-move-read-args "donate" t
223      (lambda (commits)
224        (list (magit-read-other-branch (format "Move %s cherries to branch"
225                                               (length commits)))))))
226   (magit--cherry-move commits (magit-get-current-branch) branch args))
227
228 ;;;###autoload
229 (defun magit-cherry-spinout (commits branch start-point &optional args)
230   "Move COMMITS from the current branch onto a new BRANCH.
231 Remove COMMITS from the current branch and stay on that branch.
232 If a conflict occurs, then you have to fix that and finish the
233 process manually."
234   (interactive (magit--cherry-spinoff-read-args "spinout"))
235   (magit--cherry-move commits (magit-get-current-branch) branch args
236                       start-point))
237
238 ;;;###autoload
239 (defun magit-cherry-spinoff (commits branch start-point &optional args)
240   "Move COMMITS from the current branch onto a new BRANCH.
241 Remove COMMITS from the current branch and checkout BRANCH.
242 If a conflict occurs, then you have to fix that and finish
243 the process manually."
244   (interactive (magit--cherry-spinoff-read-args "spinoff"))
245   (magit--cherry-move commits (magit-get-current-branch) branch args
246                       start-point t))
247
248 (defun magit--cherry-move (commits src dst args
249                                    &optional start-point checkout-dst)
250   (let ((current (magit-get-current-branch)))
251     (unless (magit-branch-p dst)
252       (let ((magit-process-raise-error t))
253         (magit-call-git "branch" dst start-point))
254       (--when-let (magit-get-indirect-upstream-branch start-point)
255         (magit-call-git "branch" "--set-upstream-to" it dst)))
256     (unless (equal dst current)
257       (let ((magit-process-raise-error t))
258         (magit-call-git "checkout" dst)))
259     (if (not src) ; harvest only
260         (magit--cherry-pick commits args)
261       (let ((tip (car (last commits)))
262             (keep (concat (car commits) "^")))
263         (magit--cherry-pick commits args)
264         (set-process-sentinel
265          magit-this-process
266          (lambda (process event)
267            (when (memq (process-status process) '(exit signal))
268              (if (> (process-exit-status process) 0)
269                  (magit-process-sentinel process event)
270                (process-put process 'inhibit-refresh t)
271                (magit-process-sentinel process event)
272                (cond
273                 ((magit-rev-equal tip src)
274                  (magit-call-git "update-ref"
275                                  "-m" (format "reset: moving to %s" keep)
276                                  (magit-ref-fullname src)
277                                  keep tip)
278                  (if (not checkout-dst)
279                      (magit-run-git "checkout" src)
280                    (magit-refresh)))
281                 (t
282                  (magit-git "checkout" src)
283                  (let ((process-environment process-environment))
284                    (push (format "%s=%s -i -ne '/^pick (%s)/ or print'"
285                                  "GIT_SEQUENCE_EDITOR"
286                                  magit-perl-executable
287                                  (mapconcat #'magit-rev-abbrev commits "|"))
288                          process-environment)
289                    (magit-run-git-sequencer "rebase" "-i" keep))
290                  (when checkout-dst
291                    (set-process-sentinel
292                     magit-this-process
293                     (lambda (process event)
294                       (when (memq (process-status process) '(exit signal))
295                         (if (> (process-exit-status process) 0)
296                             (magit-process-sentinel process event)
297                           (process-put process 'inhibit-refresh t)
298                           (magit-process-sentinel process event)
299                           (magit-run-git "checkout" dst))))))))))))))))
300
301 (defun magit--cherry-pick (commits args &optional revert)
302   (let ((command (if revert "revert" "cherry-pick")))
303     (when (stringp commits)
304       (setq commits (if (string-match-p "\\.\\." commits)
305                         (split-string commits "\\.\\.")
306                       (list commits))))
307     (magit-run-git-sequencer
308      (if revert "revert" "cherry-pick")
309      (pcase-let ((`(,merge ,non-merge)
310                   (-separate 'magit-merge-commit-p commits)))
311        (cond
312         ((not merge)
313          (--remove (string-prefix-p "--mainline=" it) args))
314         (non-merge
315          (user-error "Cannot %s merge and non-merge commits at once"
316                      command))
317         ((--first (string-prefix-p "--mainline=" it) args)
318          args)
319         (t
320          (cons (format "--mainline=%s"
321                        (read-number "Replay merges relative to parent: "))
322                args))))
323      commits)))
324
325 (defun magit-cherry-pick-in-progress-p ()
326   ;; .git/sequencer/todo does not exist when there is only one commit left.
327   (file-exists-p (magit-git-dir "CHERRY_PICK_HEAD")))
328
329 ;;; Revert
330
331 ;;;###autoload (autoload 'magit-revert-popup "magit-sequence" nil t)
332 (magit-define-popup magit-revert-popup
333   "Popup console for revert commands."
334   :man-page "git-revert"
335   :switches '((?s "Add Signed-off-by lines"   "--signoff")
336               (?e "Edit commit message"       "--edit")
337               (?E "Don't edit commit message" "--no-edit"))
338   :options  '((?s "Strategy"       "--strategy=")
339               (?S "Sign using gpg" "--gpg-sign=" magit-read-gpg-secret-key)
340               (?m "Replay merge relative to parent" "--mainline="))
341   :actions  '((?V "Revert commit(s)" magit-revert-and-commit)
342               (?v "Revert changes"   magit-revert-no-commit))
343   :sequence-actions '((?V "Continue" magit-sequencer-continue)
344                       (?s "Skip"     magit-sequencer-skip)
345                       (?a "Abort"    magit-sequencer-abort))
346   :sequence-predicate 'magit-sequencer-in-progress-p
347   :default-arguments '("--edit"))
348
349 (defun magit-revert-read-args (prompt)
350   (list (or (magit-region-values 'commit)
351             (magit-read-branch-or-commit prompt))
352         (magit-revert-arguments)))
353
354 ;;;###autoload
355 (defun magit-revert-and-commit (commit &optional args)
356   "Revert COMMIT by creating a new commit.
357 Prompt for a commit, defaulting to the commit at point.  If
358 the region selects multiple commits, then revert all of them,
359 without prompting."
360   (interactive (magit-revert-read-args "Revert commit"))
361   (magit--cherry-pick commit args t))
362
363 ;;;###autoload
364 (defun magit-revert-no-commit (commit &optional args)
365   "Revert COMMIT by applying it in reverse to the worktree.
366 Prompt for a commit, defaulting to the commit at point.  If
367 the region selects multiple commits, then revert all of them,
368 without prompting."
369   (interactive (magit-revert-read-args "Revert changes"))
370   (magit--cherry-pick commit (cons "--no-commit" args) t))
371
372 (defun magit-revert-in-progress-p ()
373   ;; .git/sequencer/todo does not exist when there is only one commit left.
374   (file-exists-p (magit-git-dir "REVERT_HEAD")))
375
376 ;;; Patch
377
378 ;;;###autoload (autoload 'magit-am-popup "magit-sequence" nil t)
379 (magit-define-popup magit-am-popup
380   "Popup console for mailbox commands."
381   :man-page "git-am"
382   :switches '((?3 "Fall back on 3way merge"           "--3way")
383               (?s "Add Signed-off-by lines"           "--signoff")
384               (?c "Remove text before scissors line"  "--scissors")
385               (?k "Inhibit removal of email cruft"    "--keep")
386               (?b "Limit removal of email cruft"      "--keep-non-patch")
387               (?d "Use author date as committer date"
388                   "--committer-date-is-author-date")
389               (?D "Use committer date as author date" "--ignore-date"))
390   :options  '((?p "Remove leading slashes from paths" "-p"
391                   magit-read-number-string))
392   :actions  '((?m "Apply maildir"     magit-am-apply-maildir)
393               (?w "Apply patches"     magit-am-apply-patches)
394               (?a "Apply plain patch" magit-patch-apply-popup))
395   :default-arguments '("--3way")
396   :default-actions 'magit-am-apply-patches
397   :max-action-columns 1
398   :sequence-actions '((?w "Continue" magit-am-continue)
399                       (?s "Skip"     magit-am-skip)
400                       (?a "Abort"    magit-am-abort))
401   :sequence-predicate 'magit-am-in-progress-p)
402
403 ;;;###autoload
404 (defun magit-am-apply-patches (&optional files args)
405   "Apply the patches FILES."
406   (interactive (list (or (magit-region-values 'file)
407                          (list (let ((default (magit-file-at-point)))
408                                  (read-file-name
409                                   (if default
410                                       (format "Apply patch (%s): " default)
411                                     "Apply patch: ")
412                                   nil default))))
413                      (magit-am-arguments)))
414   (magit-run-git-sequencer "am" args "--"
415                            (--map (magit-convert-filename-for-git
416                                    (expand-file-name it))
417                                   files)))
418
419 ;;;###autoload
420 (defun magit-am-apply-maildir (&optional maildir args)
421   "Apply the patches from MAILDIR."
422   (interactive (list (read-file-name "Apply mbox or Maildir: ")
423                      (magit-am-arguments)))
424   (magit-run-git-sequencer "am" args (magit-convert-filename-for-git
425                                       (expand-file-name maildir))))
426
427 ;;;###autoload
428 (defun magit-am-continue ()
429   "Resume the current patch applying sequence."
430   (interactive)
431   (if (magit-am-in-progress-p)
432       (if (magit-anything-unstaged-p t)
433           (error "Cannot continue due to unstaged changes")
434         (magit-run-git-sequencer "am" "--continue"))
435     (user-error "Not applying any patches")))
436
437 ;;;###autoload
438 (defun magit-am-skip ()
439   "Skip the stopped at patch during a patch applying sequence."
440   (interactive)
441   (if (magit-am-in-progress-p)
442       (magit-run-git-sequencer "am" "--skip")
443     (user-error "Not applying any patches")))
444
445 ;;;###autoload
446 (defun magit-am-abort ()
447   "Abort the current patch applying sequence.
448 This discards all changes made since the sequence started."
449   (interactive)
450   (if (magit-am-in-progress-p)
451       (magit-run-git "am" "--abort")
452     (user-error "Not applying any patches")))
453
454 (defun magit-am-in-progress-p ()
455   (file-exists-p (magit-git-dir "rebase-apply/applying")))
456
457 ;;; Rebase
458
459 ;;;###autoload (autoload 'magit-rebase-popup "magit-sequence" nil t)
460 (magit-define-popup magit-rebase-popup
461   "Key menu for rebasing."
462   :man-page "git-rebase"
463   :switches '((?k "Keep empty commits"       "--keep-empty")
464               (?p "Preserve merges"          "--preserve-merges")
465               (?c "Lie about committer date" "--committer-date-is-author-date")
466               (?a "Autosquash"               "--autosquash")
467               (?A "Autostash"                "--autostash")
468               (?i "Interactive"              "--interactive")
469               (?h "Disable hooks"            "--no-verify"))
470   :actions  '((lambda ()
471                 (concat (propertize "Rebase " 'face 'magit-popup-heading)
472                         (propertize (or (magit-get-current-branch) "HEAD")
473                                     'face 'magit-branch-local)
474                         (propertize " onto" 'face 'magit-popup-heading)))
475               (?p (lambda ()
476                     (--when-let (magit-get-push-branch) (concat it "\n")))
477                   magit-rebase-onto-pushremote)
478               (?u (lambda ()
479                     (--when-let (magit-get-upstream-branch) (concat it "\n")))
480                   magit-rebase-onto-upstream)
481               (?e "elsewhere" magit-rebase-branch)
482               "Rebase"
483               (?i "interactively"      magit-rebase-interactive)
484               (?m "to modify a commit" magit-rebase-edit-commit)
485               (?s "a subset"           magit-rebase-subset)
486               (?w "to reword a commit" magit-rebase-reword-commit) nil
487               (?k "to remove a commit" magit-rebase-remove-commit) nil
488               (?f "to autosquash"      magit-rebase-autosquash))
489   :sequence-actions '((?r "Continue" magit-rebase-continue)
490                       (?s "Skip"     magit-rebase-skip)
491                       (?e "Edit"     magit-rebase-edit)
492                       (?a "Abort"    magit-rebase-abort))
493   :sequence-predicate 'magit-rebase-in-progress-p
494   :max-action-columns 2)
495
496 (defun magit-git-rebase (target args)
497   (magit-run-git-sequencer "rebase" target args))
498
499 ;;;###autoload
500 (defun magit-rebase-onto-pushremote (args)
501   "Rebase the current branch onto `branch.<name>.pushRemote'.
502 If that variable is unset, then rebase onto `remote.pushDefault'."
503   (interactive (list (magit-rebase-arguments)))
504   (--if-let (magit-get-current-branch)
505       (if-let ((remote (magit-get-push-remote it)))
506           (if (member remote (magit-list-remotes))
507               (magit-git-rebase (concat remote "/" it) args)
508             (user-error "Remote `%s' doesn't exist" remote))
509         (user-error "No push-remote is configured for %s" it))
510     (user-error "No branch is checked out")))
511
512 ;;;###autoload
513 (defun magit-rebase-onto-upstream (args)
514   "Rebase the current branch onto its upstream branch."
515   (interactive (list (magit-rebase-arguments)))
516   (--if-let (magit-get-current-branch)
517       (if-let ((target (magit-get-upstream-branch it)))
518           (magit-git-rebase target args)
519         (user-error "No upstream is configured for %s" it))
520     (user-error "No branch is checked out")))
521
522 ;;;###autoload
523 (defun magit-rebase-branch (target args)
524   "Rebase the current branch onto a branch read in the minibuffer.
525 All commits that are reachable from `HEAD' but not from the
526 selected branch TARGET are being rebased."
527   (interactive (list (magit-read-other-branch-or-commit "Rebase onto")
528                      (magit-rebase-arguments)))
529   (message "Rebasing...")
530   (magit-git-rebase target args)
531   (message "Rebasing...done"))
532
533 ;;;###autoload
534 (defun magit-rebase-subset (newbase start args)
535   "Rebase a subset of the current branch's history onto a new base.
536 Rebase commits from START to `HEAD' onto NEWBASE.
537 START has to be selected from a list of recent commits."
538   (interactive (list (magit-read-other-branch-or-commit
539                       "Rebase subset onto" nil
540                       (magit-get-upstream-branch))
541                      nil
542                      (magit-rebase-arguments)))
543   (if start
544       (progn (message "Rebasing...")
545              (magit-run-git-sequencer "rebase" "--onto" newbase start args)
546              (message "Rebasing...done"))
547     (magit-log-select
548       `(lambda (commit)
549          (magit-rebase-subset ,newbase (concat commit "^") (list ,@args)))
550       (concat "Type %p on a commit to rebase it "
551               "and commits above it onto " newbase ","))))
552
553 (defun magit-rebase-interactive-1
554     (commit args message &optional editor delay-edit-confirm noassert confirm)
555   (declare (indent 2))
556   (when commit
557     (if (eq commit :merge-base)
558         (setq commit (--if-let (magit-get-upstream-branch)
559                          (magit-git-string "merge-base" it "HEAD")
560                        nil))
561       (unless (magit-rev-ancestor-p commit "HEAD")
562         (user-error "%s isn't an ancestor of HEAD" commit))
563       (if (magit-commit-parents commit)
564           (setq commit (concat commit "^"))
565         (setq args (cons "--root" args)))))
566   (when (and commit (not noassert))
567     (setq commit (magit-rebase-interactive-assert commit delay-edit-confirm)))
568   (if (and commit (not confirm))
569       (let ((process-environment process-environment))
570         (when editor
571           (push (concat "GIT_SEQUENCE_EDITOR=" editor) process-environment))
572         (magit-run-git-sequencer "rebase" "-i" args
573                                  (unless (member "--root" args) commit)))
574     (magit-log-select
575       `(lambda (commit)
576          (magit-rebase-interactive-1 commit (list ,@args)
577            ,message ,editor ,delay-edit-confirm ,noassert))
578       message)))
579
580 (defvar magit--rebase-published-symbol nil)
581 (defvar magit--rebase-public-edit-confirmed nil)
582
583 (defun magit-rebase-interactive-assert (since &optional delay-edit-confirm)
584   (let* ((commit (if (string-suffix-p "^" since)
585                      ;; If SINCE is "REV^", then the user selected
586                      ;; "REV", which is the first commit that will
587                      ;; be replaced. (from^..to] <=> [from..to].
588                      (substring since 0 -1)
589                    ;; The "--root" argument is being used.
590                    since))
591          (branches (magit-list-publishing-branches commit)))
592     (setq magit--rebase-public-edit-confirmed
593           (delete (magit-toplevel) magit--rebase-public-edit-confirmed))
594     (when (and branches
595                (or (not delay-edit-confirm)
596                    ;; The user might have stopped at a published commit
597                    ;; merely to add new commits *after* it.  Try not to
598                    ;; ask users whether they really want to edit public
599                    ;; commits, when they don't actually intend to do so.
600                    (not (--all-p (magit-rev-equal it commit) branches))))
601       (let ((m1 "Some of these commits have already been published to ")
602             (m2 ".\nDo you really want to modify them"))
603         (magit-confirm (or magit--rebase-published-symbol 'rebase-published)
604           (concat m1 "%s" m2)
605           (concat m1 "%i public branches" m2)
606           nil branches))
607       (push (magit-toplevel) magit--rebase-public-edit-confirmed)))
608   (if (magit-git-lines "rev-list" "--merges" (concat since "..HEAD"))
609       (magit-read-char-case "Proceed despite merge in rebase range?  " nil
610         (?c "[c]ontinue" since)
611         (?s "[s]elect other" nil)
612         (?a "[a]bort" (user-error "Quit")))
613     since))
614
615 ;;;###autoload
616 (defun magit-rebase-interactive (commit args)
617   "Start an interactive rebase sequence."
618   (interactive (list (magit-commit-at-point)
619                      (magit-rebase-arguments)))
620   (magit-rebase-interactive-1 commit args
621     "Type %p on a commit to rebase it and all commits above it,"
622     nil t))
623
624 ;;;###autoload
625 (defun magit-rebase-autosquash (args)
626   "Combine squash and fixup commits with their intended targets."
627   (interactive (list (magit-rebase-arguments)))
628   (magit-rebase-interactive-1 :merge-base (cons "--autosquash" args)
629     "Type %p on a commit to squash into it and then rebase as necessary,"
630     "true" nil t))
631
632 ;;;###autoload
633 (defun magit-rebase-edit-commit (commit args)
634   "Edit a single older commit using rebase."
635   (interactive (list (magit-commit-at-point)
636                      (magit-rebase-arguments)))
637   (magit-rebase-interactive-1 commit args
638     "Type %p on a commit to edit it,"
639     (concat magit-perl-executable
640             " -i -p -e '++$x if not $x and s/^pick/edit/'")
641     t))
642
643 ;;;###autoload
644 (defun magit-rebase-reword-commit (commit args)
645   "Reword a single older commit using rebase."
646   (interactive (list (magit-commit-at-point)
647                      (magit-rebase-arguments)))
648   (magit-rebase-interactive-1 commit args
649     "Type %p on a commit to reword its message,"
650     (concat magit-perl-executable
651             " -i -p -e '++$x if not $x and s/^pick/reword/'")))
652
653 ;;;###autoload
654 (defun magit-rebase-remove-commit (commit args)
655   "Remove a single older commit using rebase."
656   (interactive (list (magit-commit-at-point)
657                      (magit-rebase-arguments)))
658   (magit-rebase-interactive-1 commit args
659     "Type %p on a commit to remove it,"
660     (concat magit-perl-executable
661             " -i -p -e '++$x if not $x and s/^pick/# pick/'")
662     nil nil t))
663
664 ;;;###autoload
665 (defun magit-rebase-continue (&optional noedit)
666   "Restart the current rebasing operation.
667 In some cases this pops up a commit message buffer for you do
668 edit.  With a prefix argument the old message is reused as-is."
669   (interactive "P")
670   (if (magit-rebase-in-progress-p)
671       (if (magit-anything-unstaged-p t)
672           (user-error "Cannot continue rebase with unstaged changes")
673         (when (and (magit-anything-staged-p)
674                    (file-exists-p (magit-git-dir "rebase-merge"))
675                    (not (member (magit-toplevel)
676                                 magit--rebase-public-edit-confirmed)))
677           (magit-commit-amend-assert))
678         (if noedit
679             (let ((process-environment process-environment))
680               (push "GIT_EDITOR=true" process-environment)
681               (magit-run-git-async (magit--rebase-resume-command) "--continue")
682               (set-process-sentinel magit-this-process
683                                     #'magit-sequencer-process-sentinel)
684               magit-this-process)
685           (magit-run-git-sequencer (magit--rebase-resume-command) "--continue")))
686     (user-error "No rebase in progress")))
687
688 ;;;###autoload
689 (defun magit-rebase-skip ()
690   "Skip the current commit and restart the current rebase operation."
691   (interactive)
692   (unless (magit-rebase-in-progress-p)
693     (user-error "No rebase in progress"))
694   (magit-run-git-sequencer (magit--rebase-resume-command) "--skip"))
695
696 ;;;###autoload
697 (defun magit-rebase-edit ()
698   "Edit the todo list of the current rebase operation."
699   (interactive)
700   (unless (magit-rebase-in-progress-p)
701     (user-error "No rebase in progress"))
702   (magit-run-git-sequencer "rebase" "--edit-todo"))
703
704 ;;;###autoload
705 (defun magit-rebase-abort ()
706   "Abort the current rebase operation, restoring the original branch."
707   (interactive)
708   (unless (magit-rebase-in-progress-p)
709     (user-error "No rebase in progress"))
710   (magit-confirm 'abort-rebase "Abort this rebase")
711   (magit-run-git (magit--rebase-resume-command) "--abort"))
712
713 (defun magit-rebase-in-progress-p ()
714   "Return t if a rebase is in progress."
715   (or (file-exists-p (magit-git-dir "rebase-merge"))
716       (file-exists-p (magit-git-dir "rebase-apply/onto"))))
717
718 (defun magit--rebase-resume-command ()
719   (if (file-exists-p (magit-git-dir "rebase-recursive")) "rbr" "rebase"))
720
721 ;;; Sections
722
723 (defun magit-insert-sequencer-sequence ()
724   "Insert section for the on-going cherry-pick or revert sequence.
725 If no such sequence is in progress, do nothing."
726   (let ((picking (magit-cherry-pick-in-progress-p)))
727     (when (or picking (magit-revert-in-progress-p))
728       (magit-insert-section (sequence)
729         (magit-insert-heading (if picking "Cherry Picking" "Reverting"))
730         (when-let ((lines
731                     (cdr (magit-file-lines (magit-git-dir "sequencer/todo")))))
732           (dolist (line (nreverse lines))
733             (when (string-match
734                    "^\\(pick\\|revert\\) \\([^ ]+\\) \\(.*\\)$" line)
735               (magit-bind-match-strings (cmd hash msg) line
736                 (magit-insert-section (commit hash)
737                   (insert (propertize cmd 'face 'magit-sequence-pick)
738                           " " (propertize hash 'face 'magit-hash)
739                           " " msg "\n"))))))
740         (magit-sequence-insert-sequence
741          (magit-file-line (magit-git-dir (if picking
742                                              "CHERRY_PICK_HEAD"
743                                            "REVERT_HEAD")))
744          (magit-file-line (magit-git-dir "sequencer/head")))
745         (insert "\n")))))
746
747 (defun magit-insert-am-sequence ()
748   "Insert section for the on-going patch applying sequence.
749 If no such sequence is in progress, do nothing."
750   (when (magit-am-in-progress-p)
751     (magit-insert-section (rebase-sequence)
752       (magit-insert-heading "Applying patches")
753       (let ((patches (nreverse (magit-rebase-patches)))
754             patch commit)
755         (while patches
756           (setq patch (pop patches))
757           (setq commit (magit-rev-verify-commit
758                         (cadr (split-string (magit-file-line patch)))))
759           (cond ((and commit patches)
760                  (magit-sequence-insert-commit
761                   "pick" commit 'magit-sequence-pick))
762                 (patches
763                  (magit-sequence-insert-am-patch
764                   "pick" patch 'magit-sequence-pick))
765                 (commit
766                  (magit-sequence-insert-sequence commit "ORIG_HEAD"))
767                 (t
768                  (magit-sequence-insert-am-patch
769                   "stop" patch 'magit-sequence-stop)
770                  (magit-sequence-insert-sequence nil "ORIG_HEAD")))))
771       (insert ?\n))))
772
773 (defun magit-sequence-insert-am-patch (type patch face)
774   (magit-insert-section (file patch)
775     (let ((title
776            (with-temp-buffer
777              (insert-file-contents patch nil nil 4096)
778              (unless (re-search-forward "^Subject: " nil t)
779                (goto-char (point-min)))
780              (buffer-substring (point) (line-end-position)))))
781       (insert (propertize type 'face face)
782               ?\s (propertize (file-name-nondirectory patch) 'face 'magit-hash)
783               ?\s title
784               ?\n))))
785
786 (defun magit-insert-rebase-sequence ()
787   "Insert section for the on-going rebase sequence.
788 If no such sequence is in progress, do nothing."
789   (when (magit-rebase-in-progress-p)
790     (let* ((interactive (file-directory-p (magit-git-dir "rebase-merge")))
791            (dir  (if interactive "rebase-merge/" "rebase-apply/"))
792            (name (-> (concat dir "head-name") magit-git-dir magit-file-line))
793            (onto (-> (concat dir "onto")      magit-git-dir magit-file-line))
794            (onto (or (magit-rev-name onto name)
795                      (magit-rev-name onto "refs/heads/*") onto))
796            (name (or (magit-rev-name name "refs/heads/*") name)))
797       (magit-insert-section (rebase-sequence)
798         (magit-insert-heading (format "Rebasing %s onto %s" name onto))
799         (if interactive
800             (magit-rebase-insert-merge-sequence onto)
801           (magit-rebase-insert-apply-sequence onto))
802         (insert ?\n)))))
803
804 (defun magit-rebase-insert-merge-sequence (onto)
805   (let (exec)
806     (dolist (line (nreverse
807                    (magit-file-lines
808                     (magit-git-dir "rebase-merge/git-rebase-todo"))))
809       (cond ((string-prefix-p "exec" line)
810              (setq exec (substring line 5)))
811             ((string-match (format "^\\([^%c ]+\\) \\([^ ]+\\) .*$"
812                                    (string-to-char
813                                     (or (magit-get "core.commentChar") "#")))
814                            line)
815              (magit-bind-match-strings (action hash) line
816                (unless (equal action "exec")
817                  (magit-sequence-insert-commit
818                   action hash 'magit-sequence-pick exec)))
819              (setq exec nil)))))
820   (magit-sequence-insert-sequence
821    (magit-file-line (magit-git-dir "rebase-merge/stopped-sha"))
822    onto
823    (--when-let (magit-file-lines (magit-git-dir "rebase-merge/done"))
824      (cadr (split-string (car (last it)))))))
825
826 (defun magit-rebase-insert-apply-sequence (onto)
827   (let ((rewritten
828          (--map (car (split-string it))
829                 (magit-file-lines (magit-git-dir "rebase-apply/rewritten"))))
830         (stop (magit-file-line (magit-git-dir "rebase-apply/original-commit"))))
831     (dolist (patch (nreverse (cdr (magit-rebase-patches))))
832       (let ((hash (cadr (split-string (magit-file-line patch)))))
833         (unless (or (member hash rewritten)
834                     (equal hash stop))
835           (magit-sequence-insert-commit "pick" hash 'magit-sequence-pick)))))
836   (magit-sequence-insert-sequence
837    (magit-file-line (magit-git-dir "rebase-apply/original-commit"))
838    onto))
839
840 (defun magit-rebase-patches ()
841   (directory-files (magit-git-dir "rebase-apply") t "^[0-9]\\{4\\}$"))
842
843 (defun magit-sequence-insert-sequence (stop onto &optional orig)
844   (let ((head (magit-rev-parse "HEAD")) done)
845     (setq onto (if onto (magit-rev-parse onto) head))
846     (setq done (magit-git-lines "log" "--format=%H" (concat onto "..HEAD")))
847     (when (and stop (not (member stop done)))
848       (let ((id (magit-patch-id stop)))
849         (--if-let (--first (equal (magit-patch-id it) id) done)
850             (setq stop it)
851           (cond
852            ((--first (magit-rev-equal it stop) done)
853             ;; The commit's testament has been executed.
854             (magit-sequence-insert-commit "void" stop 'magit-sequence-drop))
855            ;; The faith of the commit is still undecided...
856            ((magit-anything-unmerged-p)
857             ;; ...and time travel isn't for the faint of heart.
858             (magit-sequence-insert-commit "join" stop 'magit-sequence-part))
859            ((magit-anything-modified-p t)
860             ;; ...and the dust hasn't settled yet...
861             (magit-sequence-insert-commit
862              (let* ((magit--refresh-cache nil)
863                     (staged   (magit-commit-tree "oO" nil "HEAD"))
864                     (unstaged (magit-commit-worktree "oO" "--reset")))
865                (cond
866                 ;; ...but we could end up at the same tree just by committing.
867                 ((or (magit-rev-equal staged   stop)
868                      (magit-rev-equal unstaged stop)) "goal")
869                 ;; ...but the changes are still there, untainted.
870                 ((or (equal (magit-patch-id staged)   id)
871                      (equal (magit-patch-id unstaged) id)) "same")
872                 ;; ...and some changes are gone and/or others were added.
873                 (t "work")))
874              stop 'magit-sequence-part))
875            ;; The commit is definitely gone...
876            ((--first (magit-rev-equal it stop) done)
877             ;; ...but all of its changes are still in effect.
878             (magit-sequence-insert-commit "poof" stop 'magit-sequence-drop))
879            (t
880             ;; ...and some changes are gone and/or other changes were added.
881             (magit-sequence-insert-commit "gone" stop 'magit-sequence-drop)))
882           (setq stop nil))))
883     (dolist (rev done)
884       (apply 'magit-sequence-insert-commit
885              (cond ((equal rev stop)
886                     ;; ...but its reincarnation lives on.
887                     ;; Or it didn't die in the first place.
888                     (list (if (and (equal rev head)
889                                    (equal (magit-patch-id rev)
890                                           (magit-patch-id orig)))
891                               "stop" ; We haven't done anything yet.
892                             "like")  ; There are new commits.
893                           rev (if (equal rev head)
894                                   'magit-sequence-head
895                                 'magit-sequence-stop)))
896                    ((equal rev head)
897                     (list "done" rev 'magit-sequence-head))
898                    (t
899                     (list "done" rev 'magit-sequence-done)))))
900     (magit-sequence-insert-commit "onto" onto
901                                 (if (equal onto head)
902                                     'magit-sequence-head
903                                   'magit-sequence-onto))))
904
905 (defun magit-sequence-insert-commit (type hash face &optional exec)
906   (magit-insert-section (commit hash)
907     (magit-insert-heading
908       (propertize type 'face face)    "\s"
909       (magit-format-rev-summary hash) "\n")
910     (when exec
911       (insert (propertize "exec" 'face 'magit-sequence-onto) "\s" exec "\n"))))
912
913 ;;; _
914 (provide 'magit-sequence)
915 ;;; magit-sequence.el ends here