Better stack rename abstraction
[stgit.git] / stgit / commands / branch.py
blob36fd2934b6d29526694ed8edad6c3aaf8d0ade53
1 import time
3 from stgit.argparse import opt
4 from stgit.commands.common import (
5 CmdException,
6 DirectoryGotoTopLevel,
7 check_conflicts,
8 check_head_top_equal,
9 check_local_changes,
10 git_commit,
12 from stgit.config import config
13 from stgit.exception import StackException
14 from stgit.lib import log
15 from stgit.lib.git.branch import Branch
16 from stgit.lib.git.repository import DetachedHeadException
17 from stgit.lib.stack import Stack, StackRepository
18 from stgit.lib.transaction import StackTransaction, TransactionHalted
19 from stgit.out import out
20 from stgit.run import RunException
22 __copyright__ = """
23 Copyright (C) 2005, Chuck Lever <cel@netapp.com>
25 This program is free software; you can redistribute it and/or modify
26 it under the terms of the GNU General Public License version 2 as
27 published by the Free Software Foundation.
29 This program is distributed in the hope that it will be useful,
30 but WITHOUT ANY WARRANTY; without even the implied warranty of
31 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 GNU General Public License for more details.
34 You should have received a copy of the GNU General Public License
35 along with this program; if not, see http://www.gnu.org/licenses/.
36 """
38 help = 'Branch operations: switch, list, create, rename, delete, ...'
39 kind = 'stack'
40 usage = [
41 '',
42 '[--merge] [--] <branch>',
43 '--list',
44 '--create [--] <new-branch> [<committish>]',
45 '--clone [--] [<new-branch>]',
46 '--rename [--] [<old-name>] <new-name>',
47 '--protect [--] [<branch>]',
48 '--unprotect [--] [<branch>]',
49 '--delete [--force] [--] <branch>',
50 '--cleanup [--force] [--] [<branch>]',
51 '--description=<description> [--] [<branch>]',
53 description = """
54 Create, clone, switch between, rename, or delete development branches
55 within a git repository.
57 'stg branch'::
58 Display the name of the current branch.
60 'stg branch' <branch>::
61 Switch to the given branch."""
63 args = ['all_branches']
64 options = [
65 opt(
66 '-l',
67 '--list',
68 action='store_true',
69 short='List the branches contained in this repository',
70 long="""
71 List each branch in the current repository, followed by its
72 branch description (if any). The current branch is prefixed
73 with '>'. Branches that have been initialized for StGit (with
74 linkstg:init[]) are prefixed with 's'. Protected branches are
75 prefixed with 'p'.""",
77 opt(
78 '-c',
79 '--create',
80 action='store_true',
81 short='Create (and switch to) a new branch',
82 long="""
83 Create (and switch to) a new branch. The new branch is already
84 initialized as an StGit patch stack, so you do not have to run
85 linkstg:init[] manually. If you give a committish argument,
86 the new branch is based there; otherwise, it is based at the
87 current HEAD.
89 StGit will try to detect the branch off of which the new
90 branch is forked, as well as the remote repository from which
91 that parent branch is taken (if any), so that running
92 linkstg:pull[] will automatically pull new commits from the
93 correct branch. It will warn if it cannot guess the parent
94 branch (e.g. if you do not specify a branch name as
95 committish).""",
97 opt(
98 '--clone',
99 action='store_true',
100 short='Clone the contents of the current branch',
101 long="""
102 Clone the current branch, under the name <new-branch> if
103 specified, or using the current branch's name plus a
104 timestamp.
106 The description of the new branch is set to tell it is a clone
107 of the current branch. The parent information of the new
108 branch is copied from the current branch.""",
110 opt(
111 '-r',
112 '--rename',
113 action='store_true',
114 short='Rename an existing branch',
116 opt(
117 '-p',
118 '--protect',
119 action='store_true',
120 short='Prevent StGit from modifying a branch',
121 long="""
122 Prevent StGit from modifying a branch -- either the current
123 one, or one named on the command line.""",
125 opt(
126 '-u',
127 '--unprotect',
128 action='store_true',
129 short='Allow StGit to modify a branch',
130 long="""
131 Allow StGit to modify a branch -- either the current one, or
132 one named on the command line. This undoes the effect of an
133 earlier 'stg branch --protect' command.""",
135 opt(
136 '--delete',
137 action='store_true',
138 short='Delete a branch',
139 long="""
140 Delete the named branch. If there are any patches left in the
141 branch, StGit will refuse to delete it unless you give the
142 '--force' flag.
144 A protected branch cannot be deleted; it must be unprotected
145 first (see '--unprotect' above).
147 If you delete the current branch, you are switched to the
148 "master" branch, if it exists.""",
150 opt(
151 '--cleanup',
152 action='store_true',
153 short='Clean up the StGit metadata for a branch',
154 long="""
155 Remove the StGit information for the current or given branch. If there
156 are patches left in the branch, StGit refuses the operation unless
157 '--force' is given.
159 A protected branch cannot be cleaned up; it must be unprotected first
160 (see '--unprotect' above).
162 A cleaned up branch can be re-initialised using the 'stg init'
163 command.""",
165 opt(
166 '-d',
167 '--description',
168 short='Set the branch description',
170 opt(
171 '--merge',
172 action='store_true',
173 short='Merge work tree changes into the other branch',
175 opt(
176 '--force',
177 action='store_true',
178 short='Force a delete when the series is not empty',
182 directory = DirectoryGotoTopLevel()
185 def __is_current_branch(branch_name):
186 try:
187 return directory.repository.current_branch_name == branch_name
188 except DetachedHeadException:
189 return False
192 def __print_branch(branch_name, length):
193 branch = Branch(directory.repository, branch_name)
194 current = '>' if __is_current_branch(branch_name) else ' '
195 try:
196 stack = directory.repository.get_stack(branch_name)
197 except StackException:
198 initialised = protected = ' '
199 else:
200 initialised = 's'
201 protected = 'p' if stack.protected else ' '
203 out.stdout(
204 current
205 + ' '
206 + initialised
207 + protected
208 + '\t'
209 + branch_name.ljust(length)
210 + ' | '
211 + (branch.get_description() or '')
215 def __delete_branch(doomed_name, force=False):
216 if __is_current_branch(doomed_name):
217 raise CmdException('Cannot delete the current branch')
219 branch = Branch(directory.repository, doomed_name)
220 try:
221 stack = directory.repository.get_stack(doomed_name)
222 except StackException:
223 stack = None
225 if stack:
226 if stack.protected:
227 raise CmdException('This branch is protected. Delete is not permitted')
228 if not force and stack.patchorder.all:
229 raise CmdException('Cannot delete: the series still contains patches')
231 out.start('Deleting branch "%s"' % doomed_name)
232 if stack:
233 stack.cleanup()
234 branch.delete()
235 out.done()
238 def __cleanup_branch(name, force=False):
239 stack = directory.repository.get_stack(name)
240 if stack.protected:
241 raise CmdException('This branch is protected. Clean up is not permitted')
242 if not force and stack.patchorder.all:
243 raise CmdException('Cannot clean up: the series still contains patches')
245 out.start('Cleaning up branch "%s"' % name)
246 stack.cleanup()
247 out.done()
250 def __create_branch(branch_name, committish):
251 repository = directory.repository
253 branch_commit = None
254 if committish is not None:
255 parentbranch = None
256 try:
257 branchpoint = repository.run(
258 ['git', 'rev-parse', '--symbolic-full-name', committish]
259 ).output_one_line()
261 if branchpoint.startswith('refs/heads/') or branchpoint.startswith(
262 'refs/remotes/'
264 # committish is a valid ref from the branchpoint setting above
265 parentbranch = committish
267 except RunException:
268 out.info(
269 'Do not know how to determine parent branch from "%s"' % committish
271 # exception in branch = rev_parse() leaves branchpoint unbound
272 branchpoint = None
274 branch_commit = git_commit(branchpoint or committish, repository)
276 if parentbranch:
277 out.info('Recording "%s" as parent branch' % parentbranch)
278 else:
279 out.info(
280 'Do not know how to determine parent branch from "%s"' % committish
282 else:
283 try:
284 # branch stack off current branch
285 parentbranch = repository.head_ref
286 except DetachedHeadException:
287 parentbranch = None
289 if parentbranch:
290 parentremote = config.get('branch.%s.remote' % parentbranch)
291 if parentremote:
292 out.info('Using remote "%s" to pull parent from' % parentremote)
293 else:
294 out.info('Recording as a local branch')
295 else:
296 # no known parent branch, can't guess the remote
297 parentremote = None
299 stack = Stack.create(
300 repository,
301 name=branch_name,
302 create_at=branch_commit,
303 parent_remote=parentremote,
304 parent_branch=parentbranch,
305 switch_to=True,
308 return stack
311 def func(parser, options, args):
312 repository = directory.repository
314 if options.create:
315 if len(args) == 0 or len(args) > 2:
316 parser.error('incorrect number of arguments')
318 branch_name = args[0]
319 committish = None if len(args) < 2 else args[1]
321 if committish:
322 check_local_changes(repository)
323 check_conflicts(repository.default_iw)
324 try:
325 stack = repository.get_stack()
326 except (DetachedHeadException, StackException):
327 pass
328 else:
329 check_head_top_equal(stack)
331 stack = __create_branch(branch_name, committish)
333 out.info('Branch "%s" created' % branch_name)
334 log.log_entry(stack, 'branch --create %s' % stack.name)
335 return
337 elif options.clone:
339 cur_branch = Branch(repository, repository.current_branch_name)
340 if len(args) == 0:
341 clone_name = cur_branch.name + time.strftime('-%C%y%m%d-%H%M%S')
342 elif len(args) == 1:
343 clone_name = args[0]
344 else:
345 parser.error('incorrect number of arguments')
347 check_local_changes(repository)
348 check_conflicts(repository.default_iw)
349 try:
350 stack = repository.current_stack
351 except StackException:
352 stack = None
353 base = repository.refs.get(repository.head_ref)
354 else:
355 check_head_top_equal(stack)
356 base = stack.base
358 out.start('Cloning current branch to "%s"' % clone_name)
359 clone = Stack.create(
360 repository,
361 name=clone_name,
362 create_at=base,
363 parent_remote=cur_branch.parent_remote,
364 parent_branch=cur_branch.name,
366 if stack:
367 for pn in stack.patchorder.all_visible:
368 patch = stack.patches.get(pn)
369 clone.patches.new(pn, patch.commit, 'clone %s' % stack.name)
370 clone.patchorder.set_order(
371 applied=[], unapplied=stack.patchorder.all_visible, hidden=[]
373 trans = StackTransaction(clone, 'clone')
374 try:
375 for pn in stack.patchorder.applied:
376 trans.push_patch(pn)
377 except TransactionHalted:
378 pass
379 trans.run()
380 prefix = 'branch.%s.' % cur_branch.name
381 new_prefix = 'branch.%s.' % clone.name
382 for n, v in list(config.getstartswith(prefix)):
383 config.set(n.replace(prefix, new_prefix, 1), v)
384 clone.set_description('clone of "%s"' % cur_branch.name)
385 clone.switch_to()
386 out.done()
388 log.copy_log(
389 StackRepository.default(), cur_branch.name, clone.name, 'branch --clone'
391 return
393 elif options.delete:
395 if len(args) != 1:
396 parser.error('incorrect number of arguments')
397 __delete_branch(args[0], options.force)
398 log.delete_log(StackRepository.default(), args[0])
399 return
401 elif options.cleanup:
403 if not args:
404 name = repository.current_branch_name
405 elif len(args) == 1:
406 name = args[0]
407 else:
408 parser.error('incorrect number of arguments')
409 __cleanup_branch(name, options.force)
410 log.delete_log(StackRepository.default(), name)
411 return
413 elif options.list:
415 if len(args) != 0:
416 parser.error('incorrect number of arguments')
418 branch_names = sorted(
419 ref.replace('refs/heads/', '', 1)
420 for ref in repository.refs
421 if ref.startswith('refs/heads/') and not ref.endswith('.stgit')
424 if branch_names:
425 out.info('Available branches:')
426 max_len = max(len(name) for name in branch_names)
427 for branch_name in branch_names:
428 __print_branch(branch_name, max_len)
429 else:
430 out.info('No branches')
431 return
433 elif options.protect:
435 if len(args) == 0:
436 branch_name = repository.current_branch_name
437 elif len(args) == 1:
438 branch_name = args[0]
439 else:
440 parser.error('incorrect number of arguments')
442 try:
443 stack = repository.get_stack(branch_name)
444 except StackException:
445 raise CmdException('Branch "%s" is not controlled by StGIT' % branch_name)
447 out.start('Protecting branch "%s"' % branch_name)
448 stack.protected = True
449 out.done()
451 return
453 elif options.rename:
455 if len(args) == 1:
456 stack = repository.current_stack
457 new_name = args[0]
458 elif len(args) == 2:
459 stack = repository.get_stack(args[0])
460 new_name = args[1]
461 else:
462 parser.error('incorrect number of arguments')
464 old_name = stack.name
465 stack.rename(new_name)
467 out.info('Renamed branch "%s" to "%s"' % (old_name, new_name))
468 return
470 elif options.unprotect:
472 if len(args) == 0:
473 branch_name = repository.current_branch_name
474 elif len(args) == 1:
475 branch_name = args[0]
476 else:
477 parser.error('incorrect number of arguments')
479 try:
480 stack = repository.get_stack(branch_name)
481 except StackException:
482 raise CmdException('Branch "%s" is not controlled by StGIT' % branch_name)
484 out.info('Unprotecting branch "%s"' % branch_name)
485 stack.protected = False
486 out.done()
488 return
490 elif options.description is not None:
492 if len(args) == 0:
493 branch_name = repository.current_branch_name
494 elif len(args) == 1:
495 branch_name = args[0]
496 else:
497 parser.error('incorrect number of arguments')
499 Branch(repository, branch_name).set_description(options.description)
500 return
502 elif len(args) == 1:
503 branch_name = args[0]
504 if branch_name == repository.current_branch_name:
505 raise CmdException(
506 'Branch "%s" is already the current branch' % branch_name
509 if not options.merge:
510 check_local_changes(repository)
511 check_conflicts(repository.default_iw)
512 try:
513 stack = repository.get_stack()
514 except StackException:
515 pass
516 else:
517 check_head_top_equal(stack)
519 out.start('Switching to branch "%s"' % branch_name)
520 Branch(repository, branch_name).switch_to()
521 out.done()
522 return
524 # default action: print the current branch
525 if len(args) != 0:
526 parser.error('incorrect number of arguments')
528 out.stdout(directory.repository.current_branch_name)