1+ import copy
2+ import itertools
3+ import re
4+ import typing
5+ from enum import Enum
6+
7+ from stgit import utils
18from stgit .argparse import opt
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 .commands .squash import squash
19+ from stgit .utils import edit_string
1020
1121__copyright__ = """
1222Copyright (C) 2005, Catalin Marinas <[email protected] > 2636
2737help = 'Move the stack base to another point in history'
2838kind = 'stack'
29- usage = ['[options] [--] < new-base-id> ' ]
39+ usage = ['[options] [--] [ new-base-id] ' ]
3040description = """
3141Pop all patches from current stack, move the stack base to the given
32- < new-base-id> and push the patches back.
42+ [ new-base-id] and push the patches back.
3343
3444If you experience merge conflicts, resolve the problem and continue
3545the rebase by executing the following sequence:
4555
4656args = ['commit' ]
4757options = [
58+ opt (
59+ '-i' ,
60+ '--interactive' ,
61+ action = 'store_true' ,
62+ short = 'Open an interactive editor to manipulate patches' ,
63+ ),
4864 opt (
4965 '-n' ,
5066 '--nopush' ,
6177
6278directory = DirectoryGotoTopLevel ()
6379
80+ INTERACTIVE_APPLY_LINE = '# --- APPLY_LINE ---'
81+
82+ INTERACTIVE_HELP_INSTRUCTIONS = """
83+ # Commands:
84+ # k, keep <patch> = do not modify this patch
85+ # s, squash <patch> = squash patch into the previous patch
86+ # d, delete <patch> = delete patch
87+ #
88+ # These lines can be re-ordered; they are executed from top to bottom.
89+ #
90+ # Patches above the APPLY_LINE are applied; other patches are kept unapplied.
91+ """
92+
93+
94+ def __get_description (stack , patch ):
95+ """Extract and return a patch's short description"""
96+ cd = stack .patches [patch ].data
97+ return cd .message_str .strip ().split ('\n ' , 1 )[0 ].rstrip ()
98+
99+
100+ class Action (Enum ):
101+ # do nothing; a 'no-op'
102+ KEEP = 0
103+ # delete the patch
104+ DELETE = 1
105+ # squash the patch into the previous patch
106+ SQUASH = 2
107+
108+
109+ Instruction = typing .NamedTuple (
110+ 'Instruction' , [('patch_name' , str ), ('action' , Action ), ('apply' , bool )]
111+ )
112+
113+
114+ def __do_rebase_interactive (repository , previously_applied_patches , check_merged ):
115+ """Opens an interactive editor, generates instruction list, and executes instructions."""
116+ stack = repository .get_stack ()
117+
118+ if len (stack .patchorder .all ) == 0 :
119+ return utils .STGIT_SUCCESS
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 .splitlines ():
147+ line = line .strip ()
148+
149+ # record when we find the APPLY_LINE so we know which patches to apply
150+ if INTERACTIVE_APPLY_LINE in line :
151+ if INTERACTIVE_APPLY_LINE == line :
152+ seen_apply_line = True
153+ else :
154+ raise CmdException ("Bad APPLY_LINE: '%s'" % line )
155+
156+ # ignore comment lines
157+ if not line or line .startswith ('#' ):
158+ continue
159+
160+ # parse a single instruction
161+ match = re .match (r'(\S+) (\S+).*' , line )
162+ if not match :
163+ raise CmdException ("Bad todo line: '%s'" % line )
164+ instruction_str , patch_name = match .groups ()
165+ if patch_name not in stack .patchorder .all :
166+ raise CmdException ("Bad patch name '%s'" % patch_name )
167+ if instruction_str in ('k' , 'keep' ):
168+ instruction_type = Action .KEEP
169+ elif instruction_str in ('d' , 'delete' ):
170+ instruction_type = Action .DELETE
171+ elif instruction_str in ('s' , 'squash' ):
172+ instruction_type = Action .SQUASH
173+ else :
174+ raise CmdException ("Unknown instruction '%s'" % instruction_str )
175+
176+ # save the instruction to execute later
177+ instructions .append (
178+ Instruction (patch_name , instruction_type , not seen_apply_line )
179+ )
180+
181+ # execute all the non-squash instructions first. we use a copy to make sure
182+ # we don't skip any instructions as we delete from the list
183+ for instruction in copy .copy (instructions ):
184+ if instruction .apply :
185+ post_rebase (stack , {instruction .patch_name }, 'rebase' , check_merged )
186+ if instruction .action == Action .DELETE :
187+ delete_patches (stack , stack .repository .default_iw , {instruction .patch_name })
188+ instructions .remove (instruction )
189+
190+ # execute squashes last because patch names may change during squashes. this
191+ # is a double loop so we can handle multiple chains of multiple squashes
192+ for index in itertools .count ():
193+ if index >= len (instructions ):
194+ break # reached the end of the instruction list
195+ # determine how many of the next N instructions are squashes
196+ squashes_found = False
197+ for num_squashes in itertools .count (1 ):
198+ if len (instructions ) <= index + num_squashes :
199+ break # reached the end of the instruction list
200+ if instructions [index + num_squashes ].action == Action .SQUASH :
201+ squashes_found = True
202+ else :
203+ break # not a squash; chain is over
204+ # execute squash
205+ if squashes_found :
206+ squashes = instructions [index : index + num_squashes ]
207+ squash (
208+ stack ,
209+ stack .repository .default_iw ,
210+ None ,
211+ None ,
212+ None ,
213+ [p .patch_name for p in squashes ],
214+ )
215+ # remove the squashed patches from the instruction set
216+ instructions = (
217+ instructions [: index + 1 ] + instructions [index + num_squashes :]
218+ )
219+ return utils .STGIT_SUCCESS
220+
64221
65222def func (parser , options , args ):
66223 """Rebase the current stack"""
67- if len (args ) != 1 :
68- parser .error ('incorrect number of arguments' )
224+
225+ if options .interactive :
226+ # destination is optional for '--interactive'
227+ if len (args ) not in (0 , 1 ):
228+ parser .error ('incorrect number of arguments' )
229+ else :
230+ if len (args ) != 1 :
231+ parser .error ('incorrect number of arguments' )
69232
70233 repository = directory .repository
71234 stack = repository .get_stack ()
@@ -74,14 +237,18 @@ def func(parser, options, args):
74237 if stack .protected :
75238 raise CmdException ('This branch is protected. Rebase is not permitted' )
76239
77- target = git_commit (args [0 ], repository )
240+ if len (args ) > 0 :
241+ target = git_commit (args [0 ], repository )
242+ else :
243+ target = stack .base
78244
79245 applied = stack .patchorder .applied
80-
81246 retval = prepare_rebase (stack , 'rebase' )
82247 if retval :
83248 return retval
84-
85249 rebase (stack , iw , target )
86- if not options .nopush :
250+
251+ if options .interactive :
252+ return __do_rebase_interactive (repository , applied , check_merged = options .merged )
253+ elif not options .nopush :
87254 return post_rebase (stack , applied , 'rebase' , check_merged = options .merged )
0 commit comments