Release notes: Turn the 0.15-rc1 release notes into 0.15 release notes
[stgit] / stgit / stack.py
CommitLineData
41a6d859
CM
1"""Basic quilt-like functionality
2"""
3
4__copyright__ = """
5Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License version 2 as
9published by the Free Software Foundation.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19"""
20
c1e4d7e0 21import sys, os, re
ed60fdae 22from email.Utils import formatdate
41a6d859 23
87c93eab 24from stgit.exception import *
41a6d859 25from stgit.utils import *
5e888f30 26from stgit.out import *
950b1095 27from stgit.run import *
1f3bb017 28from stgit import git, basedir, templates
41a6d859 29from stgit.config import config
8fce9909 30from shutil import copyfile
f5c820a8 31from stgit.lib import git as libgit, stackupgrade
41a6d859
CM
32
33# stack exception class
87c93eab 34class StackException(StgException):
41a6d859
CM
35 pass
36
6ad48e48
PBG
37class FilterUntil:
38 def __init__(self):
39 self.should_print = True
40 def __call__(self, x, until_test, prefix):
41 if until_test(x):
42 self.should_print = False
43 if self.should_print:
44 return x[0:len(prefix)] != prefix
45 return False
46
41a6d859
CM
47#
48# Functions
49#
50__comment_prefix = 'STG:'
6ad48e48 51__patch_prefix = 'STG_PATCH:'
41a6d859
CM
52
53def __clean_comments(f):
54 """Removes lines marked for status in a commit file
55 """
56 f.seek(0)
57
58 # remove status-prefixed lines
6ad48e48
PBG
59 lines = f.readlines()
60
61 patch_filter = FilterUntil()
62 until_test = lambda t: t == (__patch_prefix + '\n')
63 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
64
41a6d859
CM
65 # remove empty lines at the end
66 while len(lines) != 0 and lines[-1] == '\n':
67 del lines[-1]
68
69 f.seek(0); f.truncate()
70 f.writelines(lines)
71
ed60fdae
CM
72# TODO: move this out of the stgit.stack module, it is really for
73# higher level commands to handle the user interaction
7cc615f3 74def edit_file(series, line, comment, show_patch = True):
bd427e46 75 fname = '.stgitmsg.txt'
1f3bb017 76 tmpl = templates.get_template('patchdescr.tmpl')
41a6d859
CM
77
78 f = file(fname, 'w+')
7cc615f3
CL
79 if line:
80 print >> f, line
1f3bb017
CM
81 elif tmpl:
82 print >> f, tmpl,
41a6d859
CM
83 else:
84 print >> f
85 print >> f, __comment_prefix, comment
86 print >> f, __comment_prefix, \
87 'Lines prefixed with "%s" will be automatically removed.' \
88 % __comment_prefix
89 print >> f, __comment_prefix, \
90 'Trailing empty lines will be automatically removed.'
6ad48e48
PBG
91
92 if show_patch:
93 print >> f, __patch_prefix
94 # series.get_patch(series.get_current()).get_top()
f1c5519a
PR
95 diff_str = git.diff(rev1 = series.get_patch(series.get_current()).get_bottom())
96 f.write(diff_str)
6ad48e48
PBG
97
98 #Vim modeline must be near the end.
b83e37e0 99 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
41a6d859
CM
100 f.close()
101
83bb4e4c 102 call_editor(fname)
41a6d859
CM
103
104 f = file(fname, 'r+')
105
106 __clean_comments(f)
107 f.seek(0)
7cc615f3 108 result = f.read()
41a6d859
CM
109
110 f.close()
111 os.remove(fname)
112
7cc615f3 113 return result
41a6d859
CM
114
115#
116# Classes
117#
118
8fe7e9f0
YD
119class StgitObject:
120 """An object with stgit-like properties stored as files in a directory
121 """
122 def _set_dir(self, dir):
123 self.__dir = dir
124 def _dir(self):
125 return self.__dir
126
127 def create_empty_field(self, name):
128 create_empty_file(os.path.join(self.__dir, name))
129
130 def _get_field(self, name, multiline = False):
131 id_file = os.path.join(self.__dir, name)
132 if os.path.isfile(id_file):
133 line = read_string(id_file, multiline)
134 if line == '':
135 return None
136 else:
137 return line
138 else:
139 return None
140
141 def _set_field(self, name, value, multiline = False):
142 fname = os.path.join(self.__dir, name)
143 if value and value != '':
144 write_string(fname, value, multiline)
145 elif os.path.isfile(fname):
146 os.remove(fname)
147
1981b663 148
8fe7e9f0 149class Patch(StgitObject):
41a6d859
CM
150 """Basic patch implementation
151 """
262d31dc
KH
152 def __init_refs(self):
153 self.__top_ref = self.__refs_base + '/' + self.__name
154 self.__log_ref = self.__top_ref + '.log'
155
156 def __init__(self, name, series_dir, refs_base):
02ac3ad2 157 self.__series_dir = series_dir
41a6d859 158 self.__name = name
8fe7e9f0 159 self._set_dir(os.path.join(self.__series_dir, self.__name))
262d31dc
KH
160 self.__refs_base = refs_base
161 self.__init_refs()
41a6d859
CM
162
163 def create(self):
8fe7e9f0 164 os.mkdir(self._dir())
41a6d859 165
c26ca1b2 166 def delete(self, keep_log = False):
42cd003e
CM
167 if os.path.isdir(self._dir()):
168 for f in os.listdir(self._dir()):
169 os.remove(os.path.join(self._dir(), f))
170 os.rmdir(self._dir())
171 else:
172 out.warn('Patch directory "%s" does not exist' % self._dir())
173 try:
174 # the reference might not exist if the repository was corrupted
175 git.delete_ref(self.__top_ref)
176 except git.GitException, e:
177 out.warn(str(e))
c26ca1b2 178 if not keep_log and git.ref_exists(self.__log_ref):
262d31dc 179 git.delete_ref(self.__log_ref)
41a6d859
CM
180
181 def get_name(self):
182 return self.__name
183
e55b53e0 184 def rename(self, newname):
8fe7e9f0 185 olddir = self._dir()
262d31dc
KH
186 old_top_ref = self.__top_ref
187 old_log_ref = self.__log_ref
e55b53e0 188 self.__name = newname
8fe7e9f0 189 self._set_dir(os.path.join(self.__series_dir, self.__name))
262d31dc 190 self.__init_refs()
e55b53e0 191
262d31dc
KH
192 git.rename_ref(old_top_ref, self.__top_ref)
193 if git.ref_exists(old_log_ref):
194 git.rename_ref(old_log_ref, self.__log_ref)
8fe7e9f0 195 os.rename(olddir, self._dir())
844a1640
CM
196
197 def __update_top_ref(self, ref):
262d31dc 198 git.set_ref(self.__top_ref, ref)
757c5455
KH
199 self._set_field('top', ref)
200 self._set_field('bottom', git.get_commit(ref).get_parent())
844a1640 201
64354a2d 202 def __update_log_ref(self, ref):
262d31dc 203 git.set_ref(self.__log_ref, ref)
64354a2d 204
54b09584 205 def get_old_bottom(self):
8762753a 206 return git.get_commit(self.get_old_top()).get_parent()
54b09584 207
41a6d859 208 def get_bottom(self):
8762753a 209 return git.get_commit(self.get_top()).get_parent()
41a6d859 210
54b09584 211 def get_old_top(self):
8fe7e9f0 212 return self._get_field('top.old')
54b09584 213
41a6d859 214 def get_top(self):
6b760799 215 return git.rev_parse(self.__top_ref)
41a6d859 216
7cc615f3 217 def set_top(self, value, backup = False):
41a6d859 218 if backup:
757c5455
KH
219 curr_top = self.get_top()
220 self._set_field('top.old', curr_top)
221 self._set_field('bottom.old', git.get_commit(curr_top).get_parent())
844a1640 222 self.__update_top_ref(value)
41a6d859
CM
223
224 def restore_old_boundaries(self):
8fe7e9f0 225 top = self._get_field('top.old')
41a6d859 226
8762753a 227 if top:
844a1640 228 self.__update_top_ref(top)
a5bbc44d 229 return True
41a6d859 230 else:
a5bbc44d 231 return False
41a6d859
CM
232
233 def get_description(self):
8fe7e9f0 234 return self._get_field('description', True)
41a6d859 235
7cc615f3 236 def set_description(self, line):
8fe7e9f0 237 self._set_field('description', line, True)
41a6d859
CM
238
239 def get_authname(self):
8fe7e9f0 240 return self._get_field('authname')
41a6d859 241
7cc615f3 242 def set_authname(self, name):
8fe7e9f0 243 self._set_field('authname', name or git.author().name)
41a6d859
CM
244
245 def get_authemail(self):
8fe7e9f0 246 return self._get_field('authemail')
41a6d859 247
9e3f506f 248 def set_authemail(self, email):
8fe7e9f0 249 self._set_field('authemail', email or git.author().email)
41a6d859
CM
250
251 def get_authdate(self):
ed60fdae
CM
252 date = self._get_field('authdate')
253 if not date:
254 return date
255
256 if re.match('[0-9]+\s+[+-][0-9]+', date):
257 # Unix time (seconds) + time zone
258 secs_tz = date.split()
259 date = formatdate(int(secs_tz[0]))[:-5] + secs_tz[1]
260
261 return date
41a6d859 262
4db741b1 263 def set_authdate(self, date):
8fe7e9f0 264 self._set_field('authdate', date or git.author().date)
41a6d859
CM
265
266 def get_commname(self):
8fe7e9f0 267 return self._get_field('commname')
41a6d859 268
7cc615f3 269 def set_commname(self, name):
8fe7e9f0 270 self._set_field('commname', name or git.committer().name)
41a6d859
CM
271
272 def get_commemail(self):
8fe7e9f0 273 return self._get_field('commemail')
41a6d859 274
9e3f506f 275 def set_commemail(self, email):
8fe7e9f0 276 self._set_field('commemail', email or git.committer().email)
41a6d859 277
64354a2d 278 def get_log(self):
8fe7e9f0 279 return self._get_field('log')
64354a2d
CM
280
281 def set_log(self, value, backup = False):
8fe7e9f0 282 self._set_field('log', value)
64354a2d
CM
283 self.__update_log_ref(value)
284
47e24a74 285class PatchSet(StgitObject):
dd1b8fcc
YD
286 def __init__(self, name = None):
287 try:
288 if name:
289 self.set_name (name)
290 else:
291 self.set_name (git.get_head_file())
292 self.__base_dir = basedir.get()
293 except git.GitException, ex:
294 raise StackException, 'GIT tree not initialised: %s' % ex
295
296 self._set_dir(os.path.join(self.__base_dir, 'patches', self.get_name()))
297
47e24a74
YD
298 def get_name(self):
299 return self.__name
300 def set_name(self, name):
301 self.__name = name
302
dd1b8fcc
YD
303 def _basedir(self):
304 return self.__base_dir
305
47e24a74
YD
306 def get_head(self):
307 """Return the head of the branch
308 """
309 crt = self.get_current_patch()
310 if crt:
311 return crt.get_top()
312 else:
313 return self.get_base()
314
315 def get_protected(self):
316 return os.path.isfile(os.path.join(self._dir(), 'protected'))
317
318 def protect(self):
319 protect_file = os.path.join(self._dir(), 'protected')
320 if not os.path.isfile(protect_file):
321 create_empty_file(protect_file)
322
323 def unprotect(self):
324 protect_file = os.path.join(self._dir(), 'protected')
325 if os.path.isfile(protect_file):
326 os.remove(protect_file)
327
328 def __branch_descr(self):
329 return 'branch.%s.description' % self.get_name()
330
331 def get_description(self):
332 return config.get(self.__branch_descr()) or ''
333
334 def set_description(self, line):
335 if line:
336 config.set(self.__branch_descr(), line)
337 else:
338 config.unset(self.__branch_descr())
339
340 def head_top_equal(self):
341 """Return true if the head and the top are the same
342 """
343 crt = self.get_current_patch()
344 if not crt:
345 # we don't care, no patches applied
346 return True
347 return git.get_head() == crt.get_top()
348
349 def is_initialised(self):
350 """Checks if series is already initialised
351 """
f5c820a8
KH
352 return config.get(stackupgrade.format_version_key(self.get_name())
353 ) != None
47e24a74
YD
354
355
27827959 356def shortlog(patches):
1576d681 357 log = ''.join(Run('git', 'log', '--pretty=short',
27827959
KH
358 p.get_top(), '^%s' % p.get_bottom()).raw_output()
359 for p in patches)
1576d681 360 return Run('git', 'shortlog').raw_input(log).raw_output()
27827959 361
47e24a74 362class Series(PatchSet):
41a6d859
CM
363 """Class including the operations on series
364 """
365 def __init__(self, name = None):
40e65b92 366 """Takes a series name as the parameter.
41a6d859 367 """
dd1b8fcc 368 PatchSet.__init__(self, name)
598e9d3f
KH
369
370 # Update the branch to the latest format version if it is
371 # initialized, but don't touch it if it isn't.
f5c820a8
KH
372 stackupgrade.update_to_current_format_version(
373 libgit.Repository.default(), self.get_name())
598e9d3f 374
262d31dc 375 self.__refs_base = 'refs/patches/%s' % self.get_name()
02ac3ad2 376
8fe7e9f0
YD
377 self.__applied_file = os.path.join(self._dir(), 'applied')
378 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
841c7b2a 379 self.__hidden_file = os.path.join(self._dir(), 'hidden')
02ac3ad2
CL
380
381 # where this series keeps its patches
8fe7e9f0 382 self.__patch_dir = os.path.join(self._dir(), 'patches')
844a1640 383
ac50371b 384 # trash directory
8fe7e9f0 385 self.__trash_dir = os.path.join(self._dir(), 'trash')
ac50371b 386
c1e4d7e0
CM
387 def __patch_name_valid(self, name):
388 """Raise an exception if the patch name is not valid.
389 """
390 if not name or re.search('[^\w.-]', name):
391 raise StackException, 'Invalid patch name: "%s"' % name
392
41a6d859
CM
393 def get_patch(self, name):
394 """Return a Patch object for the given name
395 """
262d31dc 396 return Patch(name, self.__patch_dir, self.__refs_base)
41a6d859 397
4d0ba818
KH
398 def get_current_patch(self):
399 """Return a Patch object representing the topmost patch, or
400 None if there is no such patch."""
401 crt = self.get_current()
402 if not crt:
403 return None
4c0dd299 404 return self.get_patch(crt)
4d0ba818 405
41a6d859 406 def get_current(self):
4d0ba818
KH
407 """Return the name of the topmost patch, or None if there is
408 no such patch."""
532cdf94
KH
409 try:
410 applied = self.get_applied()
411 except StackException:
412 # No "applied" file: branch is not initialized.
413 return None
414 try:
415 return applied[-1]
416 except IndexError:
417 # No patches applied.
41a6d859 418 return None
41a6d859
CM
419
420 def get_applied(self):
40e65b92 421 if not os.path.isfile(self.__applied_file):
d37ff079 422 raise StackException, 'Branch "%s" not initialised' % self.get_name()
17364282 423 return read_strings(self.__applied_file)
41a6d859 424
ca216016
KH
425 def set_applied(self, applied):
426 write_strings(self.__applied_file, applied)
427
41a6d859 428 def get_unapplied(self):
40e65b92 429 if not os.path.isfile(self.__unapplied_file):
d37ff079 430 raise StackException, 'Branch "%s" not initialised' % self.get_name()
17364282 431 return read_strings(self.__unapplied_file)
41a6d859 432
ca216016
KH
433 def set_unapplied(self, unapplied):
434 write_strings(self.__unapplied_file, unapplied)
435
841c7b2a
CM
436 def get_hidden(self):
437 if not os.path.isfile(self.__hidden_file):
438 return []
17364282 439 return read_strings(self.__hidden_file)
841c7b2a 440
ba66e579 441 def get_base(self):
16d69115
KH
442 # Return the parent of the bottommost patch, if there is one.
443 if os.path.isfile(self.__applied_file):
444 bottommost = file(self.__applied_file).readline().strip()
445 if bottommost:
446 return self.get_patch(bottommost).get_bottom()
447 # No bottommost patch, so just return HEAD
448 return git.get_head()
ba66e579 449
254d99f8 450 def get_parent_remote(self):
d37ff079 451 value = config.get('branch.%s.remote' % self.get_name())
f72ad3d6
YD
452 if value:
453 return value
454 elif 'origin' in git.remotes_list():
27ac2b7e 455 out.note(('No parent remote declared for stack "%s",'
d37ff079 456 ' defaulting to "origin".' % self.get_name()),
27ac2b7e 457 ('Consider setting "branch.%s.remote" and'
82792b45 458 ' "branch.%s.merge" with "git config".'
d37ff079 459 % (self.get_name(), self.get_name())))
f72ad3d6
YD
460 return 'origin'
461 else:
d37ff079 462 raise StackException, 'Cannot find a parent remote for "%s"' % self.get_name()
254d99f8
YD
463
464 def __set_parent_remote(self, remote):
d37ff079 465 value = config.set('branch.%s.remote' % self.get_name(), remote)
254d99f8 466
8866feda 467 def get_parent_branch(self):
d37ff079 468 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
8866feda
YD
469 if value:
470 return value
471 elif git.rev_parse('heads/origin'):
27ac2b7e 472 out.note(('No parent branch declared for stack "%s",'
d37ff079 473 ' defaulting to "heads/origin".' % self.get_name()),
27ac2b7e 474 ('Consider setting "branch.%s.stgit.parentbranch"'
82792b45 475 ' with "git config".' % self.get_name()))
8866feda
YD
476 return 'heads/origin'
477 else:
d37ff079 478 raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
8866feda
YD
479
480 def __set_parent_branch(self, name):
d37ff079 481 if config.get('branch.%s.remote' % self.get_name()):
4646e7a3
YD
482 # Never set merge if remote is not set to avoid
483 # possibly-erroneous lookups into 'origin'
d37ff079
YD
484 config.set('branch.%s.merge' % self.get_name(), name)
485 config.set('branch.%s.stgit.parentbranch' % self.get_name(), name)
8866feda
YD
486
487 def set_parent(self, remote, localbranch):
488 if localbranch:
16881517
KH
489 if remote:
490 self.__set_parent_remote(remote)
8866feda 491 self.__set_parent_branch(localbranch)
4646e7a3
YD
492 # We'll enforce this later
493# else:
d37ff079 494# raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.get_name()
8866feda 495
41a6d859 496 def __patch_is_current(self, patch):
8fe7e9f0 497 return patch.get_name() == self.get_current()
41a6d859 498
ed0350be 499 def patch_applied(self, name):
41a6d859
CM
500 """Return true if the patch exists in the applied list
501 """
502 return name in self.get_applied()
503
ed0350be 504 def patch_unapplied(self, name):
41a6d859
CM
505 """Return true if the patch exists in the unapplied list
506 """
507 return name in self.get_unapplied()
508
841c7b2a
CM
509 def patch_hidden(self, name):
510 """Return true if the patch is hidden.
511 """
512 return name in self.get_hidden()
513
4d0ba818
KH
514 def patch_exists(self, name):
515 """Return true if there is a patch with the given name, false
516 otherwise."""
ca8b854c
CM
517 return self.patch_applied(name) or self.patch_unapplied(name) \
518 or self.patch_hidden(name)
4d0ba818 519
8866feda 520 def init(self, create_at=False, parent_remote=None, parent_branch=None):
41a6d859
CM
521 """Initialises the stgit series
522 """
598e9d3f 523 if self.is_initialised():
d37ff079 524 raise StackException, '%s already initialized' % self.get_name()
262d31dc 525 for d in [self._dir()]:
598e9d3f
KH
526 if os.path.exists(d):
527 raise StackException, '%s already exists' % d
fe847176 528
a22a62b6 529 if (create_at!=False):
d37ff079 530 git.create_branch(self.get_name(), create_at)
a22a62b6 531
41a6d859
CM
532 os.makedirs(self.__patch_dir)
533
8866feda 534 self.set_parent(parent_remote, parent_branch)
41a6d859 535
8fe7e9f0
YD
536 self.create_empty_field('applied')
537 self.create_empty_field('unapplied')
41a6d859 538
f5c820a8
KH
539 config.set(stackupgrade.format_version_key(self.get_name()),
540 str(stackupgrade.FORMAT_VERSION))
bad9dcfc 541
660ba985
CL
542 def rename(self, to_name):
543 """Renames a series
544 """
545 to_stack = Series(to_name)
84bf6268
CL
546
547 if to_stack.is_initialised():
d37ff079 548 raise StackException, '"%s" already exists' % to_stack.get_name()
660ba985 549
262d31dc
KH
550 patches = self.get_applied() + self.get_unapplied()
551
d37ff079 552 git.rename_branch(self.get_name(), to_name)
660ba985 553
262d31dc
KH
554 for patch in patches:
555 git.rename_ref('refs/patches/%s/%s' % (self.get_name(), patch),
556 'refs/patches/%s/%s' % (to_name, patch))
557 git.rename_ref('refs/patches/%s/%s.log' % (self.get_name(), patch),
558 'refs/patches/%s/%s.log' % (to_name, patch))
8fe7e9f0 559 if os.path.isdir(self._dir()):
dd1b8fcc 560 rename(os.path.join(self._basedir(), 'patches'),
d37ff079 561 self.get_name(), to_stack.get_name())
660ba985 562
cb5be4c3 563 # Rename the config section
337a0743
KH
564 for k in ['branch.%s', 'branch.%s.stgit']:
565 config.rename_section(k % self.get_name(), k % to_name)
cb5be4c3 566
660ba985
CL
567 self.__init__(to_name)
568
cc3db2b1
CL
569 def clone(self, target_series):
570 """Clones a series
571 """
09d8f8c5
CM
572 try:
573 # allow cloning of branches not under StGIT control
ba66e579 574 base = self.get_base()
09d8f8c5
CM
575 except:
576 base = git.get_head()
a22a62b6 577 Series(target_series).init(create_at = base)
cc3db2b1
CL
578 new_series = Series(target_series)
579
580 # generate an artificial description file
d37ff079 581 new_series.set_description('clone of "%s"' % self.get_name())
cc3db2b1
CL
582
583 # clone self's entire series as unapplied patches
09d8f8c5
CM
584 try:
585 # allow cloning of branches not under StGIT control
586 applied = self.get_applied()
587 unapplied = self.get_unapplied()
588 patches = applied + unapplied
589 patches.reverse()
590 except:
591 patches = applied = unapplied = []
cc3db2b1
CL
592 for p in patches:
593 patch = self.get_patch(p)
8fce9909
YD
594 newpatch = new_series.new_patch(p, message = patch.get_description(),
595 can_edit = False, unapplied = True,
596 bottom = patch.get_bottom(),
597 top = patch.get_top(),
598 author_name = patch.get_authname(),
599 author_email = patch.get_authemail(),
600 author_date = patch.get_authdate())
601 if patch.get_log():
27ac2b7e 602 out.info('Setting log to %s' % patch.get_log())
8fce9909
YD
603 newpatch.set_log(patch.get_log())
604 else:
27ac2b7e 605 out.info('No log for %s' % p)
cc3db2b1
CL
606
607 # fast forward the cloned series to self's top
09d8f8c5 608 new_series.forward_patches(applied)
cc3db2b1 609
f32cdac5 610 # Clone parent informations
d37ff079 611 value = config.get('branch.%s.remote' % self.get_name())
0579dae6
PR
612 if value:
613 config.set('branch.%s.remote' % target_series, value)
614
d37ff079 615 value = config.get('branch.%s.merge' % self.get_name())
0579dae6
PR
616 if value:
617 config.set('branch.%s.merge' % target_series, value)
618
d37ff079 619 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
f32cdac5
YD
620 if value:
621 config.set('branch.%s.stgit.parentbranch' % target_series, value)
622
fc804a49
CL
623 def delete(self, force = False):
624 """Deletes an stgit series
625 """
2d00440c 626 if self.is_initialised():
fc804a49
CL
627 patches = self.get_unapplied() + self.get_applied()
628 if not force and patches:
629 raise StackException, \
630 'Cannot delete: the series still contains patches'
fc804a49 631 for p in patches:
4c0dd299 632 self.get_patch(p).delete()
fc804a49 633
c177ec71
YD
634 # remove the trash directory if any
635 if os.path.exists(self.__trash_dir):
636 for fname in os.listdir(self.__trash_dir):
637 os.remove(os.path.join(self.__trash_dir, fname))
638 os.rmdir(self.__trash_dir)
ac50371b 639
8fe7e9f0 640 # FIXME: find a way to get rid of those manual removals
a9d090f4 641 # (move functionality to StgitObject ?)
84bf6268 642 if os.path.exists(self.__applied_file):
fc804a49 643 os.remove(self.__applied_file)
84bf6268 644 if os.path.exists(self.__unapplied_file):
fc804a49 645 os.remove(self.__unapplied_file)
841c7b2a
CM
646 if os.path.exists(self.__hidden_file):
647 os.remove(self.__hidden_file)
f9072c2f
YD
648 if os.path.exists(self._dir()+'/orig-base'):
649 os.remove(self._dir()+'/orig-base')
737f3549 650
fc804a49
CL
651 if not os.listdir(self.__patch_dir):
652 os.rmdir(self.__patch_dir)
653 else:
27ac2b7e 654 out.warn('Patch directory %s is not empty' % self.__patch_dir)
737f3549 655
c7728cd5 656 try:
737f3549 657 os.removedirs(self._dir())
c7728cd5 658 except OSError:
27ac2b7e
KH
659 raise StackException('Series directory %s is not empty'
660 % self._dir())
737f3549 661
ff432158
CM
662 try:
663 git.delete_branch(self.get_name())
fe79e94b 664 except git.GitException:
ff432158 665 out.warn('Could not delete branch "%s"' % self.get_name())
fc804a49 666
9a6bcbe2
KH
667 config.remove_section('branch.%s' % self.get_name())
668 config.remove_section('branch.%s.stgit' % self.get_name())
85289c08 669
026c0689 670 def refresh_patch(self, files = None, message = None, edit = False,
f02be961 671 empty = False,
026c0689 672 show_patch = False,
6ad48e48 673 cache_update = True,
41a6d859
CM
674 author_name = None, author_email = None,
675 author_date = None,
f80bef49 676 committer_name = None, committer_email = None,
cb688601 677 backup = True, sign_str = None, log = 'refresh',
6889c93c 678 notes = None, bottom = None):
692c15ef 679 """Generates a new commit for the topmost patch
41a6d859 680 """
692c15ef
DK
681 patch = self.get_current_patch()
682 if not patch:
41a6d859
CM
683 raise StackException, 'No patches applied'
684
41a6d859
CM
685 descr = patch.get_description()
686 if not (message or descr):
687 edit = True
688 descr = ''
689 elif message:
690 descr = message
691
ed60fdae
CM
692 # TODO: move this out of the stgit.stack module, it is really
693 # for higher level commands to handle the user interaction
41a6d859 694 if not message and edit:
6ad48e48 695 descr = edit_file(self, descr.rstrip(), \
41a6d859 696 'Please edit the description for patch "%s" ' \
692c15ef 697 'above.' % patch.get_name(), show_patch)
41a6d859
CM
698
699 if not author_name:
700 author_name = patch.get_authname()
701 if not author_email:
702 author_email = patch.get_authemail()
41a6d859
CM
703 if not committer_name:
704 committer_name = patch.get_commname()
705 if not committer_email:
706 committer_email = patch.get_commemail()
707
130df01a 708 descr = add_sign_line(descr, sign_str, committer_name, committer_email)
c40c3500 709
6889c93c
DK
710 if not bottom:
711 bottom = patch.get_bottom()
f80bef49 712
f02be961
DK
713 if empty:
714 tree_id = git.get_commit(bottom).get_tree()
715 else:
716 tree_id = None
717
026c0689 718 commit_id = git.commit(files = files,
f80bef49 719 message = descr, parents = [bottom],
402ad990 720 cache_update = cache_update,
f02be961
DK
721 tree_id = tree_id,
722 set_head = True,
41a6d859
CM
723 allowempty = True,
724 author_name = author_name,
725 author_email = author_email,
726 author_date = author_date,
727 committer_name = committer_name,
728 committer_email = committer_email)
729
f80bef49 730 patch.set_top(commit_id, backup = backup)
84fcbc3b
CM
731 patch.set_description(descr)
732 patch.set_authname(author_name)
733 patch.set_authemail(author_email)
734 patch.set_authdate(author_date)
735 patch.set_commname(committer_name)
736 patch.set_commemail(committer_email)
c14444b9 737
64354a2d 738 if log:
eff17c6b 739 self.log_patch(patch, log, notes)
64354a2d 740
c14444b9 741 return commit_id
41a6d859 742
37a4d1bf
CM
743 def new_patch(self, name, message = None, can_edit = True,
744 unapplied = False, show_patch = False,
0ec93bfd 745 top = None, bottom = None, commit = True,
41a6d859 746 author_name = None, author_email = None, author_date = None,