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