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