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