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