Use --force to overwrite python files
[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
41a6d859
CM
22
23from stgit.utils import *
5e888f30 24from stgit.out import *
1f3bb017 25from stgit import git, basedir, templates
41a6d859 26from stgit.config import config
8fce9909 27from shutil import copyfile
41a6d859
CM
28
29
30# stack exception class
31class StackException(Exception):
32 pass
33
6ad48e48
PBG
34class FilterUntil:
35 def __init__(self):
36 self.should_print = True
37 def __call__(self, x, until_test, prefix):
38 if until_test(x):
39 self.should_print = False
40 if self.should_print:
41 return x[0:len(prefix)] != prefix
42 return False
43
41a6d859
CM
44#
45# Functions
46#
47__comment_prefix = 'STG:'
6ad48e48 48__patch_prefix = 'STG_PATCH:'
41a6d859
CM
49
50def __clean_comments(f):
51 """Removes lines marked for status in a commit file
52 """
53 f.seek(0)
54
55 # remove status-prefixed lines
6ad48e48
PBG
56 lines = f.readlines()
57
58 patch_filter = FilterUntil()
59 until_test = lambda t: t == (__patch_prefix + '\n')
60 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
61
41a6d859
CM
62 # remove empty lines at the end
63 while len(lines) != 0 and lines[-1] == '\n':
64 del lines[-1]
65
66 f.seek(0); f.truncate()
67 f.writelines(lines)
68
7cc615f3 69def edit_file(series, line, comment, show_patch = True):
bd427e46 70 fname = '.stgitmsg.txt'
1f3bb017 71 tmpl = templates.get_template('patchdescr.tmpl')
41a6d859
CM
72
73 f = file(fname, 'w+')
7cc615f3
CL
74 if line:
75 print >> f, line
1f3bb017
CM
76 elif tmpl:
77 print >> f, tmpl,
41a6d859
CM
78 else:
79 print >> f
80 print >> f, __comment_prefix, comment
81 print >> f, __comment_prefix, \
82 'Lines prefixed with "%s" will be automatically removed.' \
83 % __comment_prefix
84 print >> f, __comment_prefix, \
85 'Trailing empty lines will be automatically removed.'
6ad48e48
PBG
86
87 if show_patch:
88 print >> f, __patch_prefix
89 # series.get_patch(series.get_current()).get_top()
90 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
91
92 #Vim modeline must be near the end.
b83e37e0 93 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
41a6d859
CM
94 f.close()
95
83bb4e4c 96 call_editor(fname)
41a6d859
CM
97
98 f = file(fname, 'r+')
99
100 __clean_comments(f)
101 f.seek(0)
7cc615f3 102 result = f.read()
41a6d859
CM
103
104 f.close()
105 os.remove(fname)
106
7cc615f3 107 return result
41a6d859
CM
108
109#
110# Classes
111#
112
8fe7e9f0
YD
113class StgitObject:
114 """An object with stgit-like properties stored as files in a directory
115 """
116 def _set_dir(self, dir):
117 self.__dir = dir
118 def _dir(self):
119 return self.__dir
120
121 def create_empty_field(self, name):
122 create_empty_file(os.path.join(self.__dir, name))
123
124 def _get_field(self, name, multiline = False):
125 id_file = os.path.join(self.__dir, name)
126 if os.path.isfile(id_file):
127 line = read_string(id_file, multiline)
128 if line == '':
129 return None
130 else:
131 return line
132 else:
133 return None
134
135 def _set_field(self, name, value, multiline = False):
136 fname = os.path.join(self.__dir, name)
137 if value and value != '':
138 write_string(fname, value, multiline)
139 elif os.path.isfile(fname):
140 os.remove(fname)
141
142
143class Patch(StgitObject):
41a6d859
CM
144 """Basic patch implementation
145 """
262d31dc
KH
146 def __init_refs(self):
147 self.__top_ref = self.__refs_base + '/' + self.__name
148 self.__log_ref = self.__top_ref + '.log'
149
150 def __init__(self, name, series_dir, refs_base):
02ac3ad2 151 self.__series_dir = series_dir
41a6d859 152 self.__name = name
8fe7e9f0 153 self._set_dir(os.path.join(self.__series_dir, self.__name))
262d31dc
KH
154 self.__refs_base = refs_base
155 self.__init_refs()
41a6d859
CM
156
157 def create(self):
8fe7e9f0
YD
158 os.mkdir(self._dir())
159 self.create_empty_field('bottom')
160 self.create_empty_field('top')
41a6d859
CM
161
162 def delete(self):
8fe7e9f0
YD
163 for f in os.listdir(self._dir()):
164 os.remove(os.path.join(self._dir(), f))
165 os.rmdir(self._dir())
262d31dc
KH
166 git.delete_ref(self.__top_ref)
167 if git.ref_exists(self.__log_ref):
168 git.delete_ref(self.__log_ref)
41a6d859
CM
169
170 def get_name(self):
171 return self.__name
172
e55b53e0 173 def rename(self, newname):
8fe7e9f0 174 olddir = self._dir()
262d31dc
KH
175 old_top_ref = self.__top_ref
176 old_log_ref = self.__log_ref
e55b53e0 177 self.__name = newname
8fe7e9f0 178 self._set_dir(os.path.join(self.__series_dir, self.__name))
262d31dc 179 self.__init_refs()
e55b53e0 180
262d31dc
KH
181 git.rename_ref(old_top_ref, self.__top_ref)
182 if git.ref_exists(old_log_ref):
183 git.rename_ref(old_log_ref, self.__log_ref)
8fe7e9f0 184 os.rename(olddir, self._dir())
844a1640
CM
185
186 def __update_top_ref(self, ref):
262d31dc 187 git.set_ref(self.__top_ref, ref)
844a1640 188
64354a2d 189 def __update_log_ref(self, ref):
262d31dc 190 git.set_ref(self.__log_ref, ref)
64354a2d 191
844a1640
CM
192 def update_top_ref(self):
193 top = self.get_top()
194 if top:
195 self.__update_top_ref(top)
e55b53e0 196
54b09584 197 def get_old_bottom(self):
8fe7e9f0 198 return self._get_field('bottom.old')
54b09584 199
41a6d859 200 def get_bottom(self):
8fe7e9f0 201 return self._get_field('bottom')
41a6d859 202
7cc615f3 203 def set_bottom(self, value, backup = False):
41a6d859 204 if backup:
8fe7e9f0
YD
205 curr = self._get_field('bottom')
206 self._set_field('bottom.old', curr)
207 self._set_field('bottom', value)
41a6d859 208
54b09584 209 def get_old_top(self):
8fe7e9f0 210 return self._get_field('top.old')
54b09584 211
41a6d859 212 def get_top(self):
8fe7e9f0 213 return self._get_field('top')
41a6d859 214
7cc615f3 215 def set_top(self, value, backup = False):
41a6d859 216 if backup:
8fe7e9f0
YD
217 curr = self._get_field('top')
218 self._set_field('top.old', curr)
219 self._set_field('top', value)
844a1640 220 self.__update_top_ref(value)
41a6d859
CM
221
222 def restore_old_boundaries(self):
8fe7e9f0
YD
223 bottom = self._get_field('bottom.old')
224 top = self._get_field('top.old')
41a6d859
CM
225
226 if top and bottom:
8fe7e9f0
YD
227 self._set_field('bottom', bottom)
228 self._set_field('top', top)
844a1640 229 self.__update_top_ref(top)
a5bbc44d 230 return True
41a6d859 231 else:
a5bbc44d 232 return False
41a6d859
CM
233
234 def get_description(self):
8fe7e9f0 235 return self._get_field('description', True)
41a6d859 236
7cc615f3 237 def set_description(self, line):
8fe7e9f0 238 self._set_field('description', line, True)
41a6d859
CM
239
240 def get_authname(self):
8fe7e9f0 241 return self._get_field('authname')
41a6d859 242
7cc615f3 243 def set_authname(self, name):
8fe7e9f0 244 self._set_field('authname', name or git.author().name)
41a6d859
CM
245
246 def get_authemail(self):
8fe7e9f0 247 return self._get_field('authemail')
41a6d859 248
9e3f506f 249 def set_authemail(self, email):
8fe7e9f0 250 self._set_field('authemail', email or git.author().email)
41a6d859
CM
251
252 def get_authdate(self):
8fe7e9f0 253 return self._get_field('authdate')
41a6d859 254
4db741b1 255 def set_authdate(self, date):
8fe7e9f0 256 self._set_field('authdate', date or git.author().date)
41a6d859
CM
257
258 def get_commname(self):
8fe7e9f0 259 return self._get_field('commname')
41a6d859 260
7cc615f3 261 def set_commname(self, name):
8fe7e9f0 262 self._set_field('commname', name or git.committer().name)
41a6d859
CM
263
264 def get_commemail(self):
8fe7e9f0 265 return self._get_field('commemail')
41a6d859 266
9e3f506f 267 def set_commemail(self, email):
8fe7e9f0 268 self._set_field('commemail', email or git.committer().email)
41a6d859 269
64354a2d 270 def get_log(self):
8fe7e9f0 271 return self._get_field('log')
64354a2d
CM
272
273 def set_log(self, value, backup = False):
8fe7e9f0 274 self._set_field('log', value)
64354a2d
CM
275 self.__update_log_ref(value)
276
598e9d3f
KH
277# The current StGIT metadata format version.
278FORMAT_VERSION = 2
279
47e24a74 280class PatchSet(StgitObject):
dd1b8fcc
YD
281 def __init__(self, name = None):
282 try:
283 if name:
284 self.set_name (name)
285 else:
286 self.set_name (git.get_head_file())
287 self.__base_dir = basedir.get()
288 except git.GitException, ex:
289 raise StackException, 'GIT tree not initialised: %s' % ex
290
291 self._set_dir(os.path.join(self.__base_dir, 'patches', self.get_name()))
292
47e24a74
YD
293 def get_name(self):
294 return self.__name
295 def set_name(self, name):
296 self.__name = name
297
dd1b8fcc
YD
298 def _basedir(self):
299 return self.__base_dir
300
47e24a74
YD
301 def get_head(self):
302 """Return the head of the branch
303 """
304 crt = self.get_current_patch()
305 if crt:
306 return crt.get_top()
307 else:
308 return self.get_base()
309
310 def get_protected(self):
311 return os.path.isfile(os.path.join(self._dir(), 'protected'))
312
313 def protect(self):
314 protect_file = os.path.join(self._dir(), 'protected')
315 if not os.path.isfile(protect_file):
316 create_empty_file(protect_file)
317
318 def unprotect(self):
319 protect_file = os.path.join(self._dir(), 'protected')
320 if os.path.isfile(protect_file):
321 os.remove(protect_file)
322
323 def __branch_descr(self):
324 return 'branch.%s.description' % self.get_name()
325
326 def get_description(self):
327 return config.get(self.__branch_descr()) or ''
328
329 def set_description(self, line):
330 if line:
331 config.set(self.__branch_descr(), line)
332 else:
333 config.unset(self.__branch_descr())
334
335 def head_top_equal(self):
336 """Return true if the head and the top are the same
337 """
338 crt = self.get_current_patch()
339 if not crt:
340 # we don't care, no patches applied
341 return True
342 return git.get_head() == crt.get_top()
343
344 def is_initialised(self):
345 """Checks if series is already initialised
346 """
9171769c 347 return bool(config.get(self.format_version_key()))
47e24a74
YD
348
349
350class Series(PatchSet):
41a6d859
CM
351 """Class including the operations on series
352 """
353 def __init__(self, name = None):
40e65b92 354 """Takes a series name as the parameter.
41a6d859 355 """
dd1b8fcc 356 PatchSet.__init__(self, name)
598e9d3f
KH
357
358 # Update the branch to the latest format version if it is
359 # initialized, but don't touch it if it isn't.
9171769c 360 self.update_to_current_format_version()
598e9d3f 361
262d31dc 362 self.__refs_base = 'refs/patches/%s' % self.get_name()
02ac3ad2 363
8fe7e9f0
YD
364 self.__applied_file = os.path.join(self._dir(), 'applied')
365 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
841c7b2a 366 self.__hidden_file = os.path.join(self._dir(), 'hidden')
02ac3ad2
CL
367
368 # where this series keeps its patches
8fe7e9f0 369 self.__patch_dir = os.path.join(self._dir(), 'patches')
844a1640 370
ac50371b 371 # trash directory
8fe7e9f0 372 self.__trash_dir = os.path.join(self._dir(), 'trash')
ac50371b 373
9171769c 374 def format_version_key(self):
69ffa22e 375 return 'branch.%s.stgit.stackformatversion' % self.get_name()
9171769c
YD
376
377 def update_to_current_format_version(self):
378 """Update a potentially older StGIT directory structure to the
379 latest version. Note: This function should depend as little as
380 possible on external functions that may change during a format
381 version bump, since it must remain able to process older formats."""
382
dd1b8fcc 383 branch_dir = os.path.join(self._basedir(), 'patches', self.get_name())
9171769c
YD
384 def get_format_version():
385 """Return the integer format version number, or None if the
386 branch doesn't have any StGIT metadata at all, of any version."""
387 fv = config.get(self.format_version_key())
69ffa22e 388 ofv = config.get('branch.%s.stgitformatversion' % self.get_name())
9171769c
YD
389 if fv:
390 # Great, there's an explicitly recorded format version
391 # number, which means that the branch is initialized and
392 # of that exact version.
393 return int(fv)
69ffa22e
YD
394 elif ofv:
395 # Old name for the version info, upgrade it
396 config.set(self.format_version_key(), ofv)
397 config.unset('branch.%s.stgitformatversion' % self.get_name())
398 return int(ofv)
9171769c
YD
399 elif os.path.isdir(os.path.join(branch_dir, 'patches')):
400 # There's a .git/patches/<branch>/patches dirctory, which
401 # means this is an initialized version 1 branch.
402 return 1
403 elif os.path.isdir(branch_dir):
404 # There's a .git/patches/<branch> directory, which means
405 # this is an initialized version 0 branch.
406 return 0
407 else:
408 # The branch doesn't seem to be initialized at all.
409 return None
410 def set_format_version(v):
411 out.info('Upgraded branch %s to format version %d' % (self.get_name(), v))
412 config.set(self.format_version_key(), '%d' % v)
413 def mkdir(d):
414 if not os.path.isdir(d):
415 os.makedirs(d)
416 def rm(f):
417 if os.path.exists(f):
418 os.remove(f)
262d31dc
KH
419 def rm_ref(ref):
420 if git.ref_exists(ref):
421 git.delete_ref(ref)
9171769c
YD
422
423 # Update 0 -> 1.
424 if get_format_version() == 0:
425 mkdir(os.path.join(branch_dir, 'trash'))
426 patch_dir = os.path.join(branch_dir, 'patches')
427 mkdir(patch_dir)
262d31dc 428 refs_base = 'refs/patches/%s' % self.get_name()
9171769c
YD
429 for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
430 + file(os.path.join(branch_dir, 'applied')).readlines()):
431 patch = patch.strip()
432 os.rename(os.path.join(branch_dir, patch),
433 os.path.join(patch_dir, patch))
262d31dc 434 Patch(patch, patch_dir, refs_base).update_top_ref()
9171769c
YD
435 set_format_version(1)
436
437 # Update 1 -> 2.
438 if get_format_version() == 1:
439 desc_file = os.path.join(branch_dir, 'description')
440 if os.path.isfile(desc_file):
441 desc = read_string(desc_file)
442 if desc:
443 config.set('branch.%s.description' % self.get_name(), desc)
444 rm(desc_file)
445 rm(os.path.join(branch_dir, 'current'))
262d31dc 446 rm_ref('refs/bases/%s' % self.get_name())
9171769c
YD
447 set_format_version(2)
448
449 # Make sure we're at the latest version.
450 if not get_format_version() in [None, FORMAT_VERSION]:
451 raise StackException('Branch %s is at format version %d, expected %d'
452 % (self.get_name(), get_format_version(), FORMAT_VERSION))
453
c1e4d7e0
CM
454 def __patch_name_valid(self, name):
455 """Raise an exception if the patch name is not valid.
456 """
457 if not name or re.search('[^\w.-]', name):
458 raise StackException, 'Invalid patch name: "%s"' % name
459
41a6d859
CM
460 def get_patch(self, name):
461 """Return a Patch object for the given name
462 """
262d31dc 463 return Patch(name, self.__patch_dir, self.__refs_base)
41a6d859 464
4d0ba818
KH
465 def get_current_patch(self):
466 """Return a Patch object representing the topmost patch, or
467 None if there is no such patch."""
468 crt = self.get_current()
469 if not crt:
470 return None
4c0dd299 471 return self.get_patch(crt)
4d0ba818 472
41a6d859 473 def get_current(self):
4d0ba818
KH
474 """Return the name of the topmost patch, or None if there is
475 no such patch."""
532cdf94
KH
476 try:
477 applied = self.get_applied()
478 except StackException:
479 # No "applied" file: branch is not initialized.
480 return None
481 try:
482 return applied[-1]
483 except IndexError:
484 # No patches applied.
41a6d859 485 return None
41a6d859
CM
486
487 def get_applied(self):
40e65b92 488 if not os.path.isfile(self.__applied_file):
d37ff079 489 raise StackException, 'Branch "%s" not initialised' % self.get_name()
17364282 490 return read_strings(self.__applied_file)
41a6d859
CM
491
492 def get_unapplied(self):
40e65b92 493 if not os.path.isfile(self.__unapplied_file):
d37ff079 494 raise StackException, 'Branch "%s" not initialised' % self.get_name()
17364282 495 return read_strings(self.__unapplied_file)
41a6d859 496
841c7b2a
CM
497 def get_hidden(self):
498 if not os.path.isfile(self.__hidden_file):
499 return []
17364282 500 return read_strings(self.__hidden_file)
841c7b2a 501
ba66e579 502 def get_base(self):
16d69115
KH
503 # Return the parent of the bottommost patch, if there is one.
504 if os.path.isfile(self.__applied_file):
505 bottommost = file(self.__applied_file).readline().strip()
506 if bottommost:
507 return self.get_patch(bottommost).get_bottom()
508 # No bottommost patch, so just return HEAD
509 return git.get_head()
ba66e579 510
254d99f8 511 def get_parent_remote(self):
d37ff079 512 value = config.get('branch.%s.remote' % self.get_name())
f72ad3d6
YD
513 if value:
514 return value
515 elif 'origin' in git.remotes_list():
27ac2b7e 516 out.note(('No parent remote declared for stack "%s",'
d37ff079 517 ' defaulting to "origin".' % self.get_name()),
27ac2b7e 518 ('Consider setting "branch.%s.remote" and'
82792b45 519 ' "branch.%s.merge" with "git config".'
d37ff079 520 % (self.get_name(), self.get_name())))
f72ad3d6
YD
521 return 'origin'
522 else:
d37ff079 523 raise StackException, 'Cannot find a parent remote for "%s"' % self.get_name()
254d99f8
YD
524
525 def __set_parent_remote(self, remote):
d37ff079 526 value = config.set('branch.%s.remote' % self.get_name(), remote)
254d99f8 527
8866feda 528 def get_parent_branch(self):
d37ff079 529 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
8866feda
YD
530 if value:
531 return value
532 elif git.rev_parse('heads/origin'):
27ac2b7e 533 out.note(('No parent branch declared for stack "%s",'
d37ff079 534 ' defaulting to "heads/origin".' % self.get_name()),
27ac2b7e 535 ('Consider setting "branch.%s.stgit.parentbranch"'
82792b45 536 ' with "git config".' % self.get_name()))
8866feda
YD
537 return 'heads/origin'
538 else:
d37ff079 539 raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
8866feda
YD
540
541 def __set_parent_branch(self, name):
d37ff079 542 if config.get('branch.%s.remote' % self.get_name()):
4646e7a3
YD
543 # Never set merge if remote is not set to avoid
544 # possibly-erroneous lookups into 'origin'
d37ff079
YD
545 config.set('branch.%s.merge' % self.get_name(), name)
546 config.set('branch.%s.stgit.parentbranch' % self.get_name(), name)
8866feda
YD
547
548 def set_parent(self, remote, localbranch):
549 if localbranch:
16881517
KH
550 if remote:
551 self.__set_parent_remote(remote)
8866feda 552 self.__set_parent_branch(localbranch)
4646e7a3
YD
553 # We'll enforce this later
554# else:
d37ff079 555# raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.get_name()
8866feda 556
41a6d859 557 def __patch_is_current(self, patch):
8fe7e9f0 558 return patch.get_name() == self.get_current()
41a6d859 559
ed0350be 560 def patch_applied(self, name):
41a6d859
CM
561 """Return true if the patch exists in the applied list
562 """
563 return name in self.get_applied()
564
ed0350be 565 def patch_unapplied(self, name):
41a6d859
CM
566 """Return true if the patch exists in the unapplied list
567 """
568 return name in self.get_unapplied()
569
841c7b2a
CM
570 def patch_hidden(self, name):
571 """Return true if the patch is hidden.
572 """
573 return name in self.get_hidden()
574
4d0ba818
KH
575 def patch_exists(self, name):
576 """Return true if there is a patch with the given name, false
577 otherwise."""
ca8b854c
CM
578 return self.patch_applied(name) or self.patch_unapplied(name) \
579 or self.patch_hidden(name)
4d0ba818 580
8866feda 581 def init(self, create_at=False, parent_remote=None, parent_branch=None):
41a6d859
CM
582 """Initialises the stgit series
583 """
598e9d3f 584 if self.is_initialised():
d37ff079 585 raise StackException, '%s already initialized' % self.get_name()
262d31dc 586 for d in [self._dir()]:
598e9d3f
KH
587 if os.path.exists(d):
588 raise StackException, '%s already exists' % d
fe847176 589
a22a62b6 590 if (create_at!=False):
d37ff079 591 git.create_branch(self.get_name(), create_at)
a22a62b6 592
41a6d859
CM
593 os.makedirs(self.__patch_dir)
594
8866feda 595 self.set_parent(parent_remote, parent_branch)
41a6d859 596
8fe7e9f0
YD
597 self.create_empty_field('applied')
598 self.create_empty_field('unapplied')
fb1cf8ec 599 self._set_field('orig-base', git.get_head())
41a6d859 600
9171769c 601 config.set(self.format_version_key(), str(FORMAT_VERSION))
bad9dcfc 602
660ba985
CL
603 def rename(self, to_name):
604 """Renames a series
605 """
606 to_stack = Series(to_name)
84bf6268
CL
607
608 if to_stack.is_initialised():
d37ff079 609 raise StackException, '"%s" already exists' % to_stack.get_name()
660ba985 610
262d31dc
KH
611 patches = self.get_applied() + self.get_unapplied()
612
d37ff079 613 git.rename_branch(self.get_name(), to_name)
660ba985 614
262d31dc
KH
615 for patch in patches:
616 git.rename_ref('refs/patches/%s/%s' % (self.get_name(), patch),
617 'refs/patches/%s/%s' % (to_name, patch))
618 git.rename_ref('refs/patches/%s/%s.log' % (self.get_name(), patch),
619 'refs/patches/%s/%s.log' % (to_name, patch))
8fe7e9f0 620 if os.path.isdir(self._dir()):
dd1b8fcc 621 rename(os.path.join(self._basedir(), 'patches'),
d37ff079 622 self.get_name(), to_stack.get_name())
660ba985 623
cb5be4c3 624 # Rename the config section
337a0743
KH
625 for k in ['branch.%s', 'branch.%s.stgit']:
626 config.rename_section(k % self.get_name(), k % to_name)
cb5be4c3 627
660ba985
CL
628 self.__init__(to_name)
629
cc3db2b1
CL
630 def clone(self, target_series):
631 """Clones a series
632 """
09d8f8c5
CM
633 try:
634 # allow cloning of branches not under StGIT control
ba66e579 635 base = self.get_base()
09d8f8c5
CM
636 except:
637 base = git.get_head()
a22a62b6 638 Series(target_series).init(create_at = base)
cc3db2b1
CL
639 new_series = Series(target_series)
640
641 # generate an artificial description file
d37ff079 642 new_series.set_description('clone of "%s"' % self.get_name())
cc3db2b1
CL
643
644 # clone self's entire series as unapplied patches
09d8f8c5
CM
645 try:
646 # allow cloning of branches not under StGIT control
647 applied = self.get_applied()
648 unapplied = self.get_unapplied()
649 patches = applied + unapplied
650 patches.reverse()
651 except:
652 patches = applied = unapplied = []
cc3db2b1
CL
653 for p in patches:
654 patch = self.get_patch(p)
8fce9909
YD
655 newpatch = new_series.new_patch(p, message = patch.get_description(),
656 can_edit = False, unapplied = True,
657 bottom = patch.get_bottom(),
658 top = patch.get_top(),
659 author_name = patch.get_authname(),
660 author_email = patch.get_authemail(),
661 author_date = patch.get_authdate())
662 if patch.get_log():
27ac2b7e 663 out.info('Setting log to %s' % patch.get_log())
8fce9909
YD
664 newpatch.set_log(patch.get_log())
665 else:
27ac2b7e 666 out.info('No log for %s' % p)
cc3db2b1
CL
667
668 # fast forward the cloned series to self's top
09d8f8c5 669 new_series.forward_patches(applied)
cc3db2b1 670
f32cdac5 671 # Clone parent informations
d37ff079 672 value = config.get('branch.%s.remote' % self.get_name())
0579dae6
PR
673 if value:
674 config.set('branch.%s.remote' % target_series, value)
675
d37ff079 676 value = config.get('branch.%s.merge' % self.get_name())
0579dae6
PR
677 if value:
678 config.set('branch.%s.merge' % target_series, value)
679
d37ff079 680 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
f32cdac5
YD
681 if value:
682 config.set('branch.%s.stgit.parentbranch' % target_series, value)
683
fc804a49
CL
684 def delete(self, force = False):
685 """Deletes an stgit series
686 """
2d00440c 687 if self.is_initialised():
fc804a49
CL
688 patches = self.get_unapplied() + self.get_applied()
689 if not force and patches:
690 raise StackException, \
691 'Cannot delete: the series still contains patches'
fc804a49 692 for p in patches:
4c0dd299 693 self.get_patch(p).delete()
fc804a49 694
c177ec71
YD
695 # remove the trash directory if any
696 if os.path.exists(self.__trash_dir):
697 for fname in os.listdir(self.__trash_dir):
698 os.remove(os.path.join(self.__trash_dir, fname))
699 os.rmdir(self.__trash_dir)
ac50371b 700
8fe7e9f0 701 # FIXME: find a way to get rid of those manual removals
a9d090f4 702 # (move functionality to StgitObject ?)
84bf6268 703 if os.path.exists(self.__applied_file):
fc804a49 704 os.remove(self.__applied_file)
84bf6268 705 if os.path.exists(self.__unapplied_file):
fc804a49 706 os.remove(self.__unapplied_file)
841c7b2a
CM
707 if os.path.exists(self.__hidden_file):
708 os.remove(self.__hidden_file)
f9072c2f
YD
709 if os.path.exists(self._dir()+'/orig-base'):
710 os.remove(self._dir()+'/orig-base')
737f3549 711
fc804a49
CL
712 if not os.listdir(self.__patch_dir):
713 os.rmdir(self.__patch_dir)
714 else:
27ac2b7e 715 out.warn('Patch directory %s is not empty' % self.__patch_dir)
737f3549 716
c7728cd5 717 try:
737f3549 718 os.removedirs(self._dir())
c7728cd5 719 except OSError:
27ac2b7e
KH
720 raise StackException('Series directory %s is not empty'
721 % self._dir())
737f3549 722
c7728cd5 723 try:
262d31dc
KH
724 git.delete_branch(self.get_name())
725 except GitException:
726 out.warn('Could not delete branch "%s"' % self.get_name())
fc804a49 727
85289c08
YD
728 # Cleanup parent informations
729 # FIXME: should one day make use of git-config --section-remove,
730 # scheduled for 1.5.1
d37ff079
YD
731 config.unset('branch.%s.remote' % self.get_name())
732 config.unset('branch.%s.merge' % self.get_name())
733 config.unset('branch.%s.stgit.parentbranch' % self.get_name())
69ffa22e 734 config.unset(self.format_version_key())
85289c08 735
026c0689
CM
736 def refresh_patch(self, files = None, message = None, edit = False,
737 show_patch = False,
6ad48e48 738 cache_update = True,
41a6d859
CM
739 author_name = None, author_email = None,
740 author_date = None,
f80bef49 741 committer_name = None, committer_email = None,
eff17c6b
CM
742 backup = False, sign_str = None, log = 'refresh',
743 notes = None):
41a6d859
CM
744 """Generates a new commit for the given patch
745 """
746 name = self.get_current()
747 if not name:
748 raise StackException, 'No patches applied'
749
4c0dd299 750 patch = self.get_patch(name)
41a6d859
CM
751
752 descr = patch.get_description()
753 if not (message or descr):
754 edit = True
755 descr = ''
756 elif message:
757 descr = message
758
759 if not message and edit:
6ad48e48 760 descr = edit_file(self, descr.rstrip(), \
41a6d859 761 'Please edit the description for patch "%s" ' \
6ad48e48 762 'above.' % name, show_patch)
41a6d859
CM
763
764 if not author_name:
765 author_name = patch.get_authname()
766 if not author_email:
767 author_email = patch.get_authemail()
768 if not author_date:
769 author_date = patch.get_authdate()
770 if not committer_name:
771 committer_name = patch.get_commname()
772 if not committer_email:
773 committer_email = patch.get_commemail()
774
c40c3500 775 if sign_str:
2f2151a6
RR
776 descr = descr.rstrip()
777 if descr.find("\nSigned-off-by:") < 0 \
778 and descr.find("\nAcked-by:") < 0:
779 descr = descr + "\n"
780
781 descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
c40c3500
CM
782 committer_name, committer_email)
783
f80bef49
CM
784 bottom = patch.get_bottom()
785
026c0689 786 commit_id = git.commit(files = files,
f80bef49 787 message = descr, parents = [bottom],
402ad990 788 cache_update = cache_update,
41a6d859
CM
789 allowempty = True,
790 author_name = author_name,
791 author_email = author_email,
792 author_date = author_date,
793 committer_name = committer_name,
794 committer_email = committer_email)
795
f80bef49
CM
796 patch.set_bottom(bottom, backup = backup)
797 patch.set_top(commit_id, backup = backup)
84fcbc3b
CM
798 patch.set_description(descr)
799 patch.set_authname(author_name)
800 patch.set_authemail(author_email)
801 patch.set_authdate(author_date)
802 patch.set_commname(committer_name)
803 patch.set_commemail(committer_email)
c14444b9 804
64354a2d 805 if log:
eff17c6b 806 self.log_patch(patch, log, notes)
64354a2d 807
c14444b9 808 return commit_id
41a6d859 809
f80bef49
CM
810 def undo_refresh(self):
811 """Undo the patch boundaries changes caused by 'refresh'
812 """
813 name = self.get_current()
814 assert(name)
815
4c0dd299 816 patch = self.get_patch(name)
f80bef49
CM
817 old_bottom = patch.get_old_bottom()
818 old_top = patch.get_old_top()
819
820 # the bottom of the patch is not changed by refresh. If the
821 # old_bottom is different, there wasn't any previous 'refresh'
822 # command (probably only a 'push')
823 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
06848fab 824 raise StackException, 'No undo information available'
f80bef49
CM
825
826 git.reset(tree_id = old_top, check_out = False)
64354a2d
CM
827 if patch.restore_old_boundaries():
828 self.log_patch(patch, 'undo')
f80bef49 829
37a4d1bf
CM
830 def new_patch(self, name, message = None, can_edit = True,
831 unapplied = False, show_patch = False,
0ec93bfd 832 top = None, bottom = None, commit = True,
41a6d859 833 author_name = None, author_email = None, author_date = None,