1+ from dataclasses import dataclass
2+ from enum import Enum
3+ import itertools
4+ import re
5+
6+ from stgit import utils
17from stgit .argparse import opt
8+ from stgit .commands .squash import squash
29from stgit .commands .common import (
310 CmdException ,
411 DirectoryGotoTopLevel ,
12+ delete_patches ,
513 git_commit ,
614 post_rebase ,
715 prepare_rebase ,
816 rebase ,
917)
18+ from stgit .utils import edit_string
1019
1120__copyright__ = """
1221Copyright (C) 2005, Catalin Marinas <[email protected] > 2635
2736help = 'Move the stack base to another point in history'
2837kind = 'stack'
29- usage = ['[options] [--] < new-base-id> ' ]
38+ usage = ['[options] [--] [ new-base-id] ' ]
3039description = """
3140Pop all patches from current stack, move the stack base to the given
32- < new-base-id> and push the patches back.
41+ [ new-base-id] and push the patches back.
3342
3443If you experience merge conflicts, resolve the problem and continue
3544the rebase by executing the following sequence:
4554
4655args = ['commit' ]
4756options = [
57+ opt (
58+ '-i' ,
59+ '--interactive' ,
60+ action = 'store_true' ,
61+ short = 'Open an interactive editor to manipulate patches' ,
62+ ),
4863 opt (
4964 '-n' ,
5065 '--nopush' ,
6176
6277directory = DirectoryGotoTopLevel ()
6378
79+ INTERACTIVE_APPLY_LINE = '# --- APPLY_LINE ---'
80+
81+ INTERACTIVE_HELP_INSTRUCTIONS = """
82+ # Commands:
83+ # k, keep <patch> = do not modify this patch
84+ # s, squash <patch> = squash patch into the previous patch
85+ # d, delete <patch> = delete patch
86+ #
87+ # These lines can be re-ordered; they are executed from top to bottom.
88+ #
89+ # Patches above the APPLY_LINE are applied; other patches are kept unapplied.
90+ #
91+ # If you remove everything the rebase will be aborted.
92+ """
93+
94+
95+ def __get_description (stack , patch ):
96+ """Extract and return a patch's short description"""
97+ cd = stack .patches [patch ].data
98+ return cd .message_str .strip ().split ('\n ' , 1 )[0 ].rstrip ()
99+
100+
101+ class Action (Enum ):
102+ # do nothing; a 'no-op'
103+ KEEP = 0
104+ # delete the patch
105+ DELETE = 1
106+ # squash the patch into the previous patch
107+ SQUASH = 2
108+
109+
110+ @dataclass
111+ class Instruction :
112+ patch_name : str
113+ action : Action
114+ apply : bool
115+
116+
117+ def __do_rebase_interactive (repository , previously_applied_patches , check_merged ):
118+ """Opens an interactive editor, generates instruction list, and executes instructions."""
119+ stack = repository .get_stack ()
120+
121+ name_len = max ((len (s ) for s in stack .patchorder .all ))
122+ line = 'keep {{:<{name_len}}} # {{}}' .format (name_len = name_len )
123+
124+ # create a list of all patches to send to the editor
125+ instructions = []
126+ for pn in previously_applied_patches :
127+ patch = (pn , __get_description (stack , pn ))
128+ instructions .append (line .format (* patch ))
129+ instructions .append (INTERACTIVE_APPLY_LINE )
130+ for pn in stack .patchorder .all :
131+ if pn in previously_applied_patches :
132+ continue
133+ patch = (pn , __get_description (stack , pn ))
134+ instructions .append (line .format (* patch ))
135+ instructions .append (INTERACTIVE_HELP_INSTRUCTIONS )
136+
137+ # open an editor to let the user generate the 'todo' instructions
138+ todo = edit_string (
139+ '\n ' .join (instructions ),
140+ '.stgit-rebase-interactive.txt' ,
141+ )
142+
143+ # parse the instructions we've been given
144+ seen_apply_line = False
145+ instructions = []
146+ for line in todo .split ('\n ' ):
147+ line = line .strip ()
148+ if line == INTERACTIVE_APPLY_LINE :
149+ seen_apply_line = True
150+
151+ # ignore comment lines
152+ if not line or line .startswith ('#' ):
153+ continue
154+
155+ # parse a single instruction
156+ match = re .match (r'(\S+) (\S+).*' , line )
157+ if not match :
158+ raise CmdException ("Bad todo line: '%s'" % line )
159+ instruction_str , patch_name = match .groups ()
160+ if patch_name not in stack .patchorder .all :
161+ raise CmdException ("Bad patch name '%s'" % patch_name )
162+ if instruction_str in ('k' , 'keep' ):
163+ instruction_type = Action .KEEP
164+ elif instruction_str in ('d' , 'delete' ):
165+ instruction_type = Action .DELETE
166+ elif instruction_str in ('s' , 'squash' ):
167+ instruction_type = Action .SQUASH
168+ else :
169+ raise CmdException ("Unknown instruction '%s'" % instruction_str )
170+
171+ # save the instruction to execute later
172+ instructions .append (
173+ Instruction (patch_name , instruction_type , not seen_apply_line )
174+ )
175+
176+ if not instructions :
177+ raise CmdException ('Nothing to do' )
178+
179+ # execute all the non-squash instructions first
180+ for i in instructions :
181+ if i .apply :
182+ post_rebase (stack , {i .patch_name }, 'rebase' , check_merged )
183+ if i .action == Action .DELETE :
184+ delete_patches (stack , stack .repository .default_iw , {i .patch_name })
185+ instructions .remove (i )
186+
187+ # execute squashes last because patch names may change during squashes. this
188+ # is a double loop so we can handle multiple chains of multiple squashes
189+ for index in itertools .count ():
190+ if len (instructions ) <= index :
191+ break # reached the end of the instruction list
192+ # determine how many of the next N instructions are squashes
193+ squashes_found = False
194+ for num_squashes in itertools .count (1 ):
195+ if len (instructions ) <= index + num_squashes :
196+ break # reached the end of the instruction list
197+ if instructions [index + num_squashes ].action == Action .SQUASH :
198+ squashes_found = True
199+ else :
200+ break # not a squash; chain is over
201+ # execute squash
202+ if squashes_found :
203+ squashes = instructions [index : index + num_squashes ]
204+ squash (
205+ stack ,
206+ stack .repository .default_iw ,
207+ None ,
208+ None ,
209+ None ,
210+ [p .patch_name for p in squashes ],
211+ )
212+ # remove the squashed patches from the instruction set
213+ instructions = (
214+ instructions [: index + 1 ] + instructions [index + num_squashes :]
215+ )
216+ return utils .STGIT_SUCCESS
217+
64218
65219def func (parser , options , args ):
66220 """Rebase the current stack"""
67- if len (args ) != 1 :
68- parser .error ('incorrect number of arguments' )
221+
222+ if options .interactive :
223+ # destination is optional for '--interactive'
224+ if len (args ) not in (0 , 1 ):
225+ parser .error ('incorrect number of arguments' )
226+ else :
227+ if len (args ) != 1 :
228+ parser .error ('incorrect number of arguments' )
69229
70230 repository = directory .repository
71231 stack = repository .get_stack ()
@@ -74,14 +234,19 @@ def func(parser, options, args):
74234 if stack .protected :
75235 raise CmdException ('This branch is protected. Rebase is not permitted' )
76236
77- target = git_commit (args [0 ], repository )
237+ if len (args ) > 0 :
238+ target = git_commit (args [0 ], repository )
239+ else :
240+ target = stack .base
78241
79242 applied = stack .patchorder .applied
80-
81243 retval = prepare_rebase (stack , 'rebase' )
82244 if retval :
83245 return retval
84-
85246 rebase (stack , iw , target )
86- if not options .nopush :
87- return post_rebase (stack , applied , 'rebase' , check_merged = options .merged )
247+
248+ if options .interactive :
249+ return __do_rebase_interactive (repository , applied , check_merged = options .merged )
250+ else :
251+ if not options .nopush :
252+ return post_rebase (stack , applied , 'rebase' , check_merged = options .merged )
0 commit comments