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