Fix deleting series trash directory
[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
CM
25from stgit.config import config
26
27
28# stack exception class
29class StackException(Exception):
30 pass
31
6ad48e48
PBG
32class FilterUntil:
33 def __init__(self):
34 self.should_print = True
35 def __call__(self, x, until_test, prefix):
36 if until_test(x):
37 self.should_print = False
38 if self.should_print:
39 return x[0:len(prefix)] != prefix
40 return False
41
41a6d859
CM
42#
43# Functions
44#
45__comment_prefix = 'STG:'
6ad48e48 46__patch_prefix = 'STG_PATCH:'
41a6d859
CM
47
48def __clean_comments(f):
49 """Removes lines marked for status in a commit file
50 """
51 f.seek(0)
52
53 # remove status-prefixed lines
6ad48e48
PBG
54 lines = f.readlines()
55
56 patch_filter = FilterUntil()
57 until_test = lambda t: t == (__patch_prefix + '\n')
58 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
59
41a6d859
CM
60 # remove empty lines at the end
61 while len(lines) != 0 and lines[-1] == '\n':
62 del lines[-1]
63
64 f.seek(0); f.truncate()
65 f.writelines(lines)
66
7cc615f3 67def edit_file(series, line, comment, show_patch = True):
bd427e46 68 fname = '.stgitmsg.txt'
1f3bb017 69 tmpl = templates.get_template('patchdescr.tmpl')
41a6d859
CM
70
71 f = file(fname, 'w+')
7cc615f3
CL
72 if line:
73 print >> f, line
1f3bb017
CM
74 elif tmpl:
75 print >> f, tmpl,
41a6d859
CM
76 else:
77 print >> f
78 print >> f, __comment_prefix, comment
79 print >> f, __comment_prefix, \
80 'Lines prefixed with "%s" will be automatically removed.' \
81 % __comment_prefix
82 print >> f, __comment_prefix, \
83 'Trailing empty lines will be automatically removed.'
6ad48e48
PBG
84
85 if show_patch:
86 print >> f, __patch_prefix
87 # series.get_patch(series.get_current()).get_top()
88 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
89
90 #Vim modeline must be near the end.
b83e37e0 91 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
41a6d859
CM
92 f.close()
93
83bb4e4c 94 call_editor(fname)
41a6d859
CM
95
96 f = file(fname, 'r+')
97
98 __clean_comments(f)
99 f.seek(0)
7cc615f3 100 result = f.read()
41a6d859
CM
101
102 f.close()
103 os.remove(fname)
104
7cc615f3 105 return result
41a6d859
CM
106
107#
108# Classes
109#
110
8fe7e9f0
YD
111class StgitObject:
112 """An object with stgit-like properties stored as files in a directory
113 """
114 def _set_dir(self, dir):
115 self.__dir = dir
116 def _dir(self):
117 return self.__dir
118
119 def create_empty_field(self, name):
120 create_empty_file(os.path.join(self.__dir, name))
121
122 def _get_field(self, name, multiline = False):
123 id_file = os.path.join(self.__dir, name)
124 if os.path.isfile(id_file):
125 line = read_string(id_file, multiline)
126 if line == '':
127 return None
128 else:
129 return line
130 else:
131 return None
132
133 def _set_field(self, name, value, multiline = False):
134 fname = os.path.join(self.__dir, name)
135 if value and value != '':
136 write_string(fname, value, multiline)
137 elif os.path.isfile(fname):
138 os.remove(fname)
139
140
141class Patch(StgitObject):
41a6d859
CM
142 """Basic patch implementation
143 """
844a1640 144 def __init__(self, name, series_dir, refs_dir):
02ac3ad2 145 self.__series_dir = series_dir
41a6d859 146 self.__name = name
8fe7e9f0 147 self._set_dir(os.path.join(self.__series_dir, self.__name))
844a1640
CM
148 self.__refs_dir = refs_dir
149 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
64354a2d
CM
150 self.__log_ref_file = os.path.join(self.__refs_dir,
151 self.__name + '.log')
41a6d859
CM
152
153 def create(self):
8fe7e9f0
YD
154 os.mkdir(self._dir())
155 self.create_empty_field('bottom')
156 self.create_empty_field('top')
41a6d859
CM
157
158 def delete(self):
8fe7e9f0
YD
159 for f in os.listdir(self._dir()):
160 os.remove(os.path.join(self._dir(), f))
161 os.rmdir(self._dir())
844a1640 162 os.remove(self.__top_ref_file)
64354a2d
CM
163 if os.path.exists(self.__log_ref_file):
164 os.remove(self.__log_ref_file)
41a6d859
CM
165
166 def get_name(self):
167 return self.__name
168
e55b53e0 169 def rename(self, newname):
8fe7e9f0 170 olddir = self._dir()
64354a2d
CM
171 old_top_ref_file = self.__top_ref_file
172 old_log_ref_file = self.__log_ref_file
e55b53e0 173 self.__name = newname
8fe7e9f0 174 self._set_dir(os.path.join(self.__series_dir, self.__name))
844a1640 175 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
64354a2d
CM
176 self.__log_ref_file = os.path.join(self.__refs_dir,
177 self.__name + '.log')
e55b53e0 178
8fe7e9f0 179 os.rename(olddir, self._dir())
64354a2d
CM
180 os.rename(old_top_ref_file, self.__top_ref_file)
181 if os.path.exists(old_log_ref_file):
182 os.rename(old_log_ref_file, self.__log_ref_file)
844a1640
CM
183
184 def __update_top_ref(self, ref):
185 write_string(self.__top_ref_file, ref)
186
64354a2d
CM
187 def __update_log_ref(self, ref):
188 write_string(self.__log_ref_file, ref)
189
844a1640
CM
190 def update_top_ref(self):
191 top = self.get_top()
192 if top:
193 self.__update_top_ref(top)
e55b53e0 194
54b09584 195 def get_old_bottom(self):
8fe7e9f0 196 return self._get_field('bottom.old')
54b09584 197
41a6d859 198 def get_bottom(self):
8fe7e9f0 199 return self._get_field('bottom')
41a6d859 200
7cc615f3 201 def set_bottom(self, value, backup = False):
41a6d859 202 if backup:
8fe7e9f0
YD
203 curr = self._get_field('bottom')
204 self._set_field('bottom.old', curr)
205 self._set_field('bottom', value)
41a6d859 206
54b09584 207 def get_old_top(self):
8fe7e9f0 208 return self._get_field('top.old')
54b09584 209
41a6d859 210 def get_top(self):
8fe7e9f0 211 return self._get_field('top')
41a6d859 212
7cc615f3 213 def set_top(self, value, backup = False):
41a6d859 214 if backup:
8fe7e9f0
YD
215 curr = self._get_field('top')
216 self._set_field('top.old', curr)
217 self._set_field('top', value)
844a1640 218 self.__update_top_ref(value)
41a6d859
CM
219
220 def restore_old_boundaries(self):
8fe7e9f0
YD
221 bottom = self._get_field('bottom.old')
222 top = self._get_field('top.old')
41a6d859
CM
223
224 if top and bottom:
8fe7e9f0
YD
225 self._set_field('bottom', bottom)
226 self._set_field('top', top)
844a1640 227 self.__update_top_ref(top)
a5bbc44d 228 return True
41a6d859 229 else:
a5bbc44d 230 return False
41a6d859
CM
231
232 def get_description(self):
8fe7e9f0 233 return self._get_field('description', True)
41a6d859 234
7cc615f3 235 def set_description(self, line):
8fe7e9f0 236 self._set_field('description', line, True)
41a6d859
CM
237
238 def get_authname(self):
8fe7e9f0 239 return self._get_field('authname')
41a6d859 240
7cc615f3 241 def set_authname(self, name):
8fe7e9f0 242 self._set_field('authname', name or git.author().name)
41a6d859
CM
243
244 def get_authemail(self):
8fe7e9f0 245 return self._get_field('authemail')
41a6d859 246
9e3f506f 247 def set_authemail(self, email):
8fe7e9f0 248 self._set_field('authemail', email or git.author().email)
41a6d859
CM
249
250 def get_authdate(self):
8fe7e9f0 251 return self._get_field('authdate')
41a6d859 252
4db741b1 253 def set_authdate(self, date):
8fe7e9f0 254 self._set_field('authdate', date or git.author().date)
41a6d859
CM
255
256 def get_commname(self):
8fe7e9f0 257 return self._get_field('commname')
41a6d859 258
7cc615f3 259 def set_commname(self, name):
8fe7e9f0 260 self._set_field('commname', name or git.committer().name)
41a6d859
CM
261
262 def get_commemail(self):
8fe7e9f0 263 return self._get_field('commemail')
41a6d859 264
9e3f506f 265 def set_commemail(self, email):
8fe7e9f0 266 self._set_field('commemail', email or git.committer().email)
41a6d859 267
64354a2d 268 def get_log(self):
8fe7e9f0 269 return self._get_field('log')
64354a2d
CM
270
271 def set_log(self, value, backup = False):
8fe7e9f0 272 self._set_field('log', value)
64354a2d
CM
273 self.__update_log_ref(value)
274
41a6d859 275
8fe7e9f0 276class Series(StgitObject):
41a6d859
CM
277 """Class including the operations on series
278 """
279 def __init__(self, name = None):
40e65b92 280 """Takes a series name as the parameter.
41a6d859 281 """
98290387
CM
282 try:
283 if name:
284 self.__name = name
285 else:
286 self.__name = git.get_head_file()
170f576b 287 self.__base_dir = basedir.get()
98290387
CM
288 except git.GitException, ex:
289 raise StackException, 'GIT tree not initialised: %s' % ex
290
8fe7e9f0 291 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
844a1640
CM
292 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
293 self.__name)
02ac3ad2 294
8fe7e9f0
YD
295 self.__applied_file = os.path.join(self._dir(), 'applied')
296 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
841c7b2a 297 self.__hidden_file = os.path.join(self._dir(), 'hidden')
8fe7e9f0
YD
298 self.__current_file = os.path.join(self._dir(), 'current')
299 self.__descr_file = os.path.join(self._dir(), 'description')
02ac3ad2
CL
300
301 # where this series keeps its patches
8fe7e9f0 302 self.__patch_dir = os.path.join(self._dir(), 'patches')
02ac3ad2 303 if not os.path.isdir(self.__patch_dir):
8fe7e9f0 304 self.__patch_dir = self._dir()
41a6d859 305
844a1640
CM
306 # if no __refs_dir, create and populate it (upgrade old repositories)
307 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
308 os.makedirs(self.__refs_dir)
309 for patch in self.get_applied() + self.get_unapplied():
310 self.get_patch(patch).update_top_ref()
311
ac50371b 312 # trash directory
8fe7e9f0 313 self.__trash_dir = os.path.join(self._dir(), 'trash')
ac50371b
CM
314 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
315 os.makedirs(self.__trash_dir)
316
c1e4d7e0
CM
317 def __patch_name_valid(self, name):
318 """Raise an exception if the patch name is not valid.
319 """
320 if not name or re.search('[^\w.-]', name):
321 raise StackException, 'Invalid patch name: "%s"' % name
322
629ddd02
CM
323 def get_branch(self):
324 """Return the branch name for the Series object
325 """
326 return self.__name
327
41a6d859
CM
328 def __set_current(self, name):
329 """Sets the topmost patch
330 """
8fe7e9f0 331 self._set_field('current', name)
41a6d859
CM
332
333 def get_patch(self, name):
334 """Return a Patch object for the given name
335 """
844a1640 336 return Patch(name, self.__patch_dir, self.__refs_dir)
41a6d859 337
4d0ba818
KH
338 def get_current_patch(self):
339 """Return a Patch object representing the topmost patch, or
340 None if there is no such patch."""
341 crt = self.get_current()
342 if not crt:
343 return None
344 return Patch(crt, self.__patch_dir, self.__refs_dir)
345
41a6d859 346 def get_current(self):
4d0ba818
KH
347 """Return the name of the topmost patch, or None if there is
348 no such patch."""
8fe7e9f0 349 name = self._get_field('current')
41a6d859
CM
350 if name == '':
351 return None
352 else:
353 return name
354
355 def get_applied(self):
40e65b92 356 if not os.path.isfile(self.__applied_file):
a2dcde71 357 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
358 f = file(self.__applied_file)
359 names = [line.strip() for line in f.readlines()]
360 f.close()
361 return names
362
363 def get_unapplied(self):
40e65b92 364 if not os.path.isfile(self.__unapplied_file):
a2dcde71 365 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
366 f = file(self.__unapplied_file)
367 names = [line.strip() for line in f.readlines()]
368 f.close()
369 return names
370
841c7b2a
CM
371 def get_hidden(self):
372 if not os.path.isfile(self.__hidden_file):
373 return []
374 f = file(self.__hidden_file)
375 names = [line.strip() for line in f.readlines()]
376 f.close()
377 return names
378
ba66e579 379 def get_base(self):
16d69115
KH
380 # Return the parent of the bottommost patch, if there is one.
381 if os.path.isfile(self.__applied_file):
382 bottommost = file(self.__applied_file).readline().strip()
383 if bottommost:
384 return self.get_patch(bottommost).get_bottom()
385 # No bottommost patch, so just return HEAD
386 return git.get_head()
ba66e579 387
e078133e
CM
388 def get_head(self):
389 """Return the head of the branch
390 """
391 crt = self.get_current_patch()
392 if crt:
393 return crt.get_top()
394 else:
395 return self.get_base()
396
0b4b9499 397 def get_protected(self):
8fe7e9f0 398 return os.path.isfile(os.path.join(self._dir(), 'protected'))
0b4b9499
CL
399
400 def protect(self):
8fe7e9f0 401 protect_file = os.path.join(self._dir(), 'protected')
0b4b9499
CL
402 if not os.path.isfile(protect_file):
403 create_empty_file(protect_file)
404
405 def unprotect(self):
8fe7e9f0 406 protect_file = os.path.join(self._dir(), 'protected')
0b4b9499
CL
407 if os.path.isfile(protect_file):
408 os.remove(protect_file)
409
c1fe1f99 410 def get_description(self):
d1368d81 411 return self._get_field('description') or ''
8fe7e9f0
YD
412
413 def set_description(self, line):
414 self._set_field('description', line)
c1fe1f99 415
254d99f8 416 def get_parent_remote(self):
f72ad3d6
YD
417 value = config.get('branch.%s.remote' % self.__name)
418 if value:
419 return value
420 elif 'origin' in git.remotes_list():
4646e7a3
YD
421 print 'Notice: no parent remote declared for stack "%s", ' \
422 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
423 'and "branch.%s.merge" with "git repo-config".' \
424 % (self.__name, self.__name, self.__name)
f72ad3d6
YD
425 return 'origin'
426 else:
427 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
254d99f8
YD
428
429 def __set_parent_remote(self, remote):
430 value = config.set('branch.%s.remote' % self.__name, remote)
431
8866feda 432 def get_parent_branch(self):
4646e7a3 433 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
8866feda
YD
434 if value:
435 return value
436 elif git.rev_parse('heads/origin'):
4646e7a3
YD
437 print 'Notice: no parent branch declared for stack "%s", ' \
438 'defaulting to "heads/origin". Consider setting ' \
439 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
c0167829 440 % (self.__name, self.__name)
8866feda
YD
441 return 'heads/origin'
442 else:
443 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
444
445 def __set_parent_branch(self, name):
4646e7a3
YD
446 if config.get('branch.%s.remote' % self.__name):
447 # Never set merge if remote is not set to avoid
448 # possibly-erroneous lookups into 'origin'
449 config.set('branch.%s.merge' % self.__name, name)
450 config.set('branch.%s.stgit.parentbranch' % self.__name, name)
8866feda
YD
451
452 def set_parent(self, remote, localbranch):
4646e7a3
YD
453 # policy: record local branches as remote='.'
454 recordremote = remote or '.'
8866feda 455 if localbranch:
4646e7a3 456 self.__set_parent_remote(recordremote)
8866feda 457 self.__set_parent_branch(localbranch)
4646e7a3
YD
458 # We'll enforce this later
459# else:
460# raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
8866feda 461
41a6d859 462 def __patch_is_current(self, patch):
8fe7e9f0 463 return patch.get_name() == self.get_current()
41a6d859 464
ed0350be 465 def patch_applied(self, name):
41a6d859
CM
466 """Return true if the patch exists in the applied list
467 """
468 return name in self.get_applied()
469
ed0350be 470 def patch_unapplied(self, name):
41a6d859
CM
471 """Return true if the patch exists in the unapplied list
472 """
473 return name in self.get_unapplied()
474
841c7b2a
CM
475 def patch_hidden(self, name):
476 """Return true if the patch is hidden.
477 """
478 return name in self.get_hidden()
479
4d0ba818
KH
480 def patch_exists(self, name):
481 """Return true if there is a patch with the given name, false
482 otherwise."""
ed0350be 483 return self.patch_applied(name) or self.patch_unapplied(name)
4d0ba818 484
41a6d859
CM
485 def head_top_equal(self):
486 """Return true if the head and the top are the same
487 """
4d0ba818 488 crt = self.get_current_patch()
41a6d859
CM
489 if not crt:
490 # we don't care, no patches applied
491 return True
4d0ba818 492 return git.get_head() == crt.get_top()
41a6d859 493
2d00440c
CL
494 def is_initialised(self):
495 """Checks if series is already initialised
496 """
497 return os.path.isdir(self.__patch_dir)
498
8866feda 499 def init(self, create_at=False, parent_remote=None, parent_branch=None):
41a6d859
CM
500 """Initialises the stgit series
501 """
fe847176 502 if os.path.exists(self.__patch_dir):
41a6d859 503 raise StackException, self.__patch_dir + ' already exists'
fe847176
YD
504 if os.path.exists(self.__refs_dir):
505 raise StackException, self.__refs_dir + ' already exists'
fe847176 506
a22a62b6
YD
507 if (create_at!=False):
508 git.create_branch(self.__name, create_at)
509
41a6d859
CM
510 os.makedirs(self.__patch_dir)
511
8866feda 512 self.set_parent(parent_remote, parent_branch)
41a6d859 513
8fe7e9f0
YD
514 self.create_empty_field('applied')
515 self.create_empty_field('unapplied')
516 self.create_empty_field('description')
517 os.makedirs(os.path.join(self._dir(), 'patches'))
844a1640 518 os.makedirs(self.__refs_dir)
b6c95ada 519 self._set_field('orig-base', git.get_head())
41a6d859 520
bad9dcfc
CL
521 def convert(self):
522 """Either convert to use a separate patch directory, or
523 unconvert to place the patches in the same directory with
524 series control files
525 """
8fe7e9f0 526 if self.__patch_dir == self._dir():
bad9dcfc
CL
527 print 'Converting old-style to new-style...',
528 sys.stdout.flush()
529
8fe7e9f0 530 self.__patch_dir = os.path.join(self._dir(), 'patches')
bad9dcfc
CL
531 os.makedirs(self.__patch_dir)
532
533 for p in self.get_applied() + self.get_unapplied():
8fe7e9f0 534 src = os.path.join(self._dir(), p)
bad9dcfc
CL
535 dest = os.path.join(self.__patch_dir, p)
536 os.rename(src, dest)
537
538 print 'done'
539
540 else:
541 print 'Converting new-style to old-style...',
542 sys.stdout.flush()
543
544 for p in self.get_applied() + self.get_unapplied():
545 src = os.path.join(self.__patch_dir, p)
8fe7e9f0 546 dest = os.path.join(self._dir(), p)
bad9dcfc
CL
547 os.rename(src, dest)
548
549 if not os.listdir(self.__patch_dir):
550 os.rmdir(self.__patch_dir)
551 print 'done'
552 else:
737f3549 553 print 'Patch directory %s is not empty.' % self.__patch_dir
bad9dcfc 554
8fe7e9f0 555 self.__patch_dir = self._dir()
bad9dcfc 556
660ba985
CL
557 def rename(self, to_name):
558 """Renames a series
559 """
560 to_stack = Series(to_name)
84bf6268
CL
561
562 if to_stack.is_initialised():
563 raise StackException, '"%s" already exists' % to_stack.get_branch()
660ba985
CL
564
565 git.rename_branch(self.__name, to_name)
566
8fe7e9f0 567 if os.path.isdir(self._dir()):