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