Some clean-up of the branch manipulation commands
[stgit] / stgit / commands / branch.py
1 """Branch command
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Chuck Lever <cel@netapp.com>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
20
21 import sys, os, time, re
22 from optparse import OptionParser, make_option
23
24 from stgit.commands.common import *
25 from stgit.utils import *
26 from stgit.out import *
27 from stgit import stack, git, basedir
28
29
30 help = 'manage patch stacks'
31 usage = """%prog [options] branch-name [commit-id]
32
33 Create, clone, switch between, rename, or delete development branches
34 within a git repository. By default, a single branch called 'master'
35 is always created in a new repository. This subcommand allows you to
36 manage several patch series in the same repository via GIT branches.
37
38 When displaying the branches, the names can be prefixed with
39 's' (StGIT managed) or 'p' (protected).
40
41 If not given any options, switch to the named branch."""
42
43 directory = DirectoryGotoToplevel()
44 options = [make_option('-c', '--create',
45 help = 'create a new development branch',
46 action = 'store_true'),
47 make_option('--clone',
48 help = 'clone the contents of the current branch',
49 action = 'store_true'),
50 make_option('--delete',
51 help = 'delete an existing development branch',
52 action = 'store_true'),
53 make_option('-d', '--description',
54 help = 'set the branch description'),
55 make_option('--force',
56 help = 'force a delete when the series is not empty',
57 action = 'store_true'),
58 make_option('-l', '--list',
59 help = 'list branches contained in this repository',
60 action = 'store_true'),
61 make_option('-p', '--protect',
62 help = 'prevent StGIT from modifying this branch',
63 action = 'store_true'),
64 make_option('-r', '--rename',
65 help = 'rename an existing development branch',
66 action = 'store_true'),
67 make_option('-u', '--unprotect',
68 help = 'allow StGIT to modify this branch',
69 action = 'store_true')]
70
71
72 def __is_current_branch(branch_name):
73 return crt_series.get_name() == branch_name
74
75 def __print_branch(branch_name, length):
76 initialized = ' '
77 current = ' '
78 protected = ' '
79
80 branch = stack.Series(branch_name)
81
82 if branch.is_initialised():
83 initialized = 's'
84 if __is_current_branch(branch_name):
85 current = '>'
86 if branch.get_protected():
87 protected = 'p'
88 out.stdout(current + ' ' + initialized + protected + '\t'
89 + branch_name.ljust(length) + ' | ' + branch.get_description())
90
91 def __delete_branch(doomed_name, force = False):
92 doomed = stack.Series(doomed_name)
93
94 if __is_current_branch(doomed_name):
95 raise CmdException('Cannot delete the current branch')
96 if doomed.get_protected():
97 raise CmdException, 'This branch is protected. Delete is not permitted'
98
99 out.start('Deleting branch "%s"' % doomed_name)
100 doomed.delete(force)
101 out.done()
102
103 def func(parser, options, args):
104
105 if options.create:
106
107 if len(args) == 0 or len(args) > 2:
108 parser.error('incorrect number of arguments')
109
110 check_local_changes()
111 check_conflicts()
112 check_head_top_equal(crt_series)
113
114 tree_id = None
115 if len(args) >= 2:
116 parentbranch = None
117 try:
118 branchpoint = git.rev_parse(args[1])
119
120 # parent branch?
121 head_re = re.compile('refs/(heads|remotes)/')
122 ref_re = re.compile(args[1] + '$')
123 for ref in git.all_refs():
124 if head_re.match(ref) and ref_re.search(ref):
125 # args[1] is a valid ref from the branchpoint
126 # setting above
127 parentbranch = args[1]
128 break;
129 except git.GitException:
130 # should use a more specific exception to catch only
131 # non-git refs ?
132 out.info('Don\'t know how to determine parent branch'
133 ' from "%s"' % args[1])
134 # exception in branch = rev_parse() leaves branchpoint unbound
135 branchpoint = None
136
137 tree_id = git_id(crt_series, branchpoint or args[1])
138
139 if parentbranch:
140 out.info('Recording "%s" as parent branch' % parentbranch)
141 else:
142 out.info('Don\'t know how to determine parent branch'
143 ' from "%s"' % args[1])
144 else:
145 # branch stack off current branch
146 parentbranch = git.get_head_file()
147
148 if parentbranch:
149 parentremote = git.identify_remote(parentbranch)
150 if parentremote:
151 out.info('Using remote "%s" to pull parent from'
152 % parentremote)
153 else:
154 out.info('Recording as a local branch')
155 else:
156 # no known parent branch, can't guess the remote
157 parentremote = None
158
159 stack.Series(args[0]).init(create_at = tree_id,
160 parent_remote = parentremote,
161 parent_branch = parentbranch)
162
163 out.info('Branch "%s" created' % args[0])
164 return
165
166 elif options.clone:
167
168 if len(args) == 0:
169 clone = crt_series.get_name() + \
170 time.strftime('-%C%y%m%d-%H%M%S')
171 elif len(args) == 1:
172 clone = args[0]
173 else:
174 parser.error('incorrect number of arguments')
175
176 check_local_changes()
177 check_conflicts()
178 check_head_top_equal(crt_series)
179
180 out.start('Cloning current branch to "%s"' % clone)
181 crt_series.clone(clone)
182 out.done()
183
184 return
185
186 elif options.delete:
187
188 if len(args) != 1:
189 parser.error('incorrect number of arguments')
190 __delete_branch(args[0], options.force)
191 return
192
193 elif options.list:
194
195 if len(args) != 0:
196 parser.error('incorrect number of arguments')
197
198 branches = git.get_heads()
199 branches.sort()
200
201 if branches:
202 out.info('Available branches:')
203 max_len = max([len(i) for i in branches])
204 for i in branches:
205 __print_branch(i, max_len)
206 else:
207 out.info('No branches')
208 return
209
210 elif options.protect:
211
212 if len(args) == 0:
213 branch_name = crt_series.get_name()
214 elif len(args) == 1:
215 branch_name = args[0]
216 else:
217 parser.error('incorrect number of arguments')
218 branch = stack.Series(branch_name)
219
220 if not branch.is_initialised():
221 raise CmdException, 'Branch "%s" is not controlled by StGIT' \
222 % branch_name
223
224 out.start('Protecting branch "%s"' % branch_name)
225 branch.protect()
226 out.done()
227
228 return
229
230 elif options.rename:
231
232 if len(args) != 2:
233 parser.error('incorrect number of arguments')
234
235 if __is_current_branch(args[0]):
236 raise CmdException, 'Renaming the current branch is not supported'
237
238 stack.Series(args[0]).rename(args[1])
239
240 out.info('Renamed branch "%s" to "%s"' % (args[0], args[1]))
241
242 return
243
244 elif options.unprotect:
245
246 if len(args) == 0:
247 branch_name = crt_series.get_name()
248 elif len(args) == 1:
249 branch_name = args[0]
250 else:
251 parser.error('incorrect number of arguments')
252 branch = stack.Series(branch_name)
253
254 if not branch.is_initialised():
255 raise CmdException, 'Branch "%s" is not controlled by StGIT' \
256 % branch_name
257
258 out.info('Unprotecting branch "%s"' % branch_name)
259 branch.unprotect()
260 out.done()
261
262 return
263
264 elif options.description is not None:
265
266 if len(args) == 0:
267 branch_name = crt_series.get_name()
268 elif len(args) == 1:
269 branch_name = args[0]
270 else:
271 parser.error('incorrect number of arguments')
272 branch = stack.Series(branch_name)
273
274 if not branch.is_initialised():
275 raise CmdException, 'Branch "%s" is not controlled by StGIT' \
276 % branch_name
277
278 branch.set_description(options.description)
279
280 return
281
282 elif len(args) == 1:
283
284 if __is_current_branch(args[0]):
285 raise CmdException, 'Branch "%s" is already the current branch' \
286 % args[0]
287
288 check_local_changes()
289 check_conflicts()
290 check_head_top_equal(crt_series)
291
292 out.start('Switching to branch "%s"' % args[0])
293 git.switch_branch(args[0])
294 out.done()
295 return
296
297 # default action: print the current branch
298 if len(args) != 0:
299 parser.error('incorrect number of arguments')
300
301 print crt_series.get_name()