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 .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+ # execute all the non-squash instructions first. we use a copy to make sure
177+ # we don't skip any instructions as we delete from the list
178+ for instruction in copy .copy (instructions ):
179+ if instruction .apply :
180+ post_rebase (stack , {instruction .patch_name }, 'rebase' , check_merged )
181+ if instruction .action == Action .DELETE :
182+ delete_patches (stack , stack .repository .default_iw , {instruction .patch_name })
183+ instructions .remove (instruction )
184+
185+ # execute squashes last because patch names may change during squashes. this
186+ # is a double loop so we can handle multiple chains of multiple squashes
187+ for index in itertools .count ():
188+ if index >= len (instructions ):
189+ break # reached the end of the instruction list
190+ # determine how many of the next N instructions are squashes
191+ squashes_found = False
192+ for num_squashes in itertools .count (1 ):
193+ if len (instructions ) <= index + num_squashes :
194+ break # reached the end of the instruction list
195+ if instructions [index + num_squashes ].action == Action .SQUASH :
196+ squashes_found = True
197+ else :
198+ break # not a squash; chain is over
199+ # execute squash
200+ if squashes_found :
201+ squashes = instructions [index : index + num_squashes ]
202+ squash (
203+ stack ,
204+ stack .repository .default_iw ,
205+ None ,
206+ None ,
207+ None ,
208+ [p .patch_name for p in squashes ],
209+ )
210+ # remove the squashed patches from the instruction set
211+ instructions = (
212+ instructions [: index + 1 ] + instructions [index + num_squashes :]
213+ )
214+ return utils .STGIT_SUCCESS
215+
64216
65217def func (parser , options , args ):
66218 """Rebase the current stack"""
67- if len (args ) != 1 :
68- parser .error ('incorrect number of arguments' )
219+
220+ if options .interactive :
221+ # destination is optional for '--interactive'
222+ if len (args ) not in (0 , 1 ):
223+ parser .error ('incorrect number of arguments' )
224+ else :
225+ if len (args ) != 1 :
226+ parser .error ('incorrect number of arguments' )
69227
70228 repository = directory .repository
71229 stack = repository .get_stack ()
@@ -74,14 +232,19 @@ def func(parser, options, args):
74232 if stack .protected :
75233 raise CmdException ('This branch is protected. Rebase is not permitted' )
76234
77- target = git_commit (args [0 ], repository )
235+ if len (args ) > 0 :
236+ target = git_commit (args [0 ], repository )
237+ else :
238+ target = stack .base
78239
79240 applied = stack .patchorder .applied
80-
81241 retval = prepare_rebase (stack , 'rebase' )
82242 if retval :
83243 return retval
84-
85244 rebase (stack , iw , target )
86- if not options .nopush :
87- return post_rebase (stack , applied , 'rebase' , check_merged = options .merged )
245+
246+ if options .interactive :
247+ return __do_rebase_interactive (repository , applied , check_merged = options .merged )
248+ else :
249+ if not options .nopush :
250+ return post_rebase (stack , applied , 'rebase' , check_merged = options .merged )
0 commit comments