Add the "smtpdelay" config option
[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
21import sys, os
22
23from stgit.utils import *
24from stgit import git
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):
41a6d859 68 fname = '.stgit.msg'
bae29ddd 69 tmpl = os.path.join(git.get_base_dir(), 'patchdescr.tmpl')
41a6d859
CM
70
71 f = file(fname, 'w+')
7cc615f3
CL
72 if line:
73 print >> f, line
41a6d859
CM
74 elif os.path.isfile(tmpl):
75 print >> f, file(tmpl).read().rstrip()
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
94 # the editor
cd076ff6
CL
95 if config.has_option('stgit', 'editor'):
96 editor = config.get('stgit', 'editor')
97 elif 'EDITOR' in os.environ:
41a6d859
CM
98 editor = os.environ['EDITOR']
99 else:
100 editor = 'vi'
101 editor += ' %s' % fname
102
103 print 'Invoking the editor: "%s"...' % editor,
104 sys.stdout.flush()
105 print 'done (exit code: %d)' % os.system(editor)
106
107 f = file(fname, 'r+')
108
109 __clean_comments(f)
110 f.seek(0)
7cc615f3 111 result = f.read()
41a6d859
CM
112
113 f.close()
114 os.remove(fname)
115
7cc615f3 116 return result
41a6d859
CM
117
118#
119# Classes
120#
121
122class Patch:
123 """Basic patch implementation
124 """
844a1640 125 def __init__(self, name, series_dir, refs_dir):
02ac3ad2 126 self.__series_dir = series_dir
41a6d859 127 self.__name = name
02ac3ad2 128 self.__dir = os.path.join(self.__series_dir, self.__name)
844a1640
CM
129 self.__refs_dir = refs_dir
130 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
41a6d859
CM
131
132 def create(self):
133 os.mkdir(self.__dir)
134 create_empty_file(os.path.join(self.__dir, 'bottom'))
135 create_empty_file(os.path.join(self.__dir, 'top'))
136
137 def delete(self):
138 for f in os.listdir(self.__dir):
139 os.remove(os.path.join(self.__dir, f))
140 os.rmdir(self.__dir)
844a1640 141 os.remove(self.__top_ref_file)
41a6d859
CM
142
143 def get_name(self):
144 return self.__name
145
e55b53e0
CM
146 def rename(self, newname):
147 olddir = self.__dir
844a1640 148 old_ref_file = self.__top_ref_file
e55b53e0 149 self.__name = newname
02ac3ad2 150 self.__dir = os.path.join(self.__series_dir, self.__name)
844a1640 151 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
e55b53e0
CM
152
153 os.rename(olddir, self.__dir)
844a1640
CM
154 os.rename(old_ref_file, self.__top_ref_file)
155
156 def __update_top_ref(self, ref):
157 write_string(self.__top_ref_file, ref)
158
159 def update_top_ref(self):
160 top = self.get_top()
161 if top:
162 self.__update_top_ref(top)
e55b53e0 163
41a6d859
CM
164 def __get_field(self, name, multiline = False):
165 id_file = os.path.join(self.__dir, name)
166 if os.path.isfile(id_file):
7cc615f3
CL
167 line = read_string(id_file, multiline)
168 if line == '':
41a6d859
CM
169 return None
170 else:
7cc615f3 171 return line
41a6d859
CM
172 else:
173 return None
174
7cc615f3 175 def __set_field(self, name, value, multiline = False):
41a6d859 176 fname = os.path.join(self.__dir, name)
7cc615f3
CL
177 if value and value != '':
178 write_string(fname, value, multiline)
41a6d859
CM
179 elif os.path.isfile(fname):
180 os.remove(fname)
181
54b09584
PBG
182 def get_old_bottom(self):
183 return self.__get_field('bottom.old')
184
41a6d859
CM
185 def get_bottom(self):
186 return self.__get_field('bottom')
187
7cc615f3 188 def set_bottom(self, value, backup = False):
41a6d859 189 if backup:
a5bbc44d 190 curr = self.__get_field('bottom')
7cc615f3 191 if curr != value:
a5bbc44d
PBG
192 self.__set_field('bottom.old', curr)
193 else:
194 self.__set_field('bottom.old', None)
7cc615f3 195 self.__set_field('bottom', value)
41a6d859 196
54b09584
PBG
197 def get_old_top(self):
198 return self.__get_field('top.old')
199
41a6d859
CM
200 def get_top(self):
201 return self.__get_field('top')
202
7cc615f3 203 def set_top(self, value, backup = False):
41a6d859 204 if backup:
a5bbc44d 205 curr = self.__get_field('top')
7cc615f3 206 if curr != value:
a5bbc44d
PBG
207 self.__set_field('top.old', curr)
208 else:
209 self.__set_field('top.old', None)
7cc615f3 210 self.__set_field('top', value)
844a1640 211 self.__update_top_ref(value)
41a6d859
CM
212
213 def restore_old_boundaries(self):
214 bottom = self.__get_field('bottom.old')
215 top = self.__get_field('top.old')
216
217 if top and bottom:
218 self.__set_field('bottom', bottom)
219 self.__set_field('top', top)
844a1640 220 self.__update_top_ref(top)
a5bbc44d 221 return True
41a6d859 222 else:
a5bbc44d 223 return False
41a6d859
CM
224
225 def get_description(self):
226 return self.__get_field('description', True)
227
7cc615f3
CL
228 def set_description(self, line):
229 self.__set_field('description', line, True)
41a6d859
CM
230
231 def get_authname(self):
232 return self.__get_field('authname')
233
7cc615f3 234 def set_authname(self, name):
4db741b1
CM
235 if not name:
236 if config.has_option('stgit', 'authname'):
237 name = config.get('stgit', 'authname')
238 elif 'GIT_AUTHOR_NAME' in os.environ:
239 name = os.environ['GIT_AUTHOR_NAME']
7cc615f3 240 self.__set_field('authname', name)
41a6d859
CM
241
242 def get_authemail(self):
243 return self.__get_field('authemail')
244
7cc615f3 245 def set_authemail(self, address):
4db741b1
CM
246 if not address:
247 if config.has_option('stgit', 'authemail'):
248 address = config.get('stgit', 'authemail')
249 elif 'GIT_AUTHOR_EMAIL' in os.environ:
250 address = os.environ['GIT_AUTHOR_EMAIL']
7cc615f3 251 self.__set_field('authemail', address)
41a6d859
CM
252
253 def get_authdate(self):
254 return self.__get_field('authdate')
255
4db741b1
CM
256 def set_authdate(self, date):
257 if not date and 'GIT_AUTHOR_DATE' in os.environ:
258 date = os.environ['GIT_AUTHOR_DATE']
259 self.__set_field('authdate', date)
41a6d859
CM
260
261 def get_commname(self):
262 return self.__get_field('commname')
263
7cc615f3 264 def set_commname(self, name):
4db741b1
CM
265 if not name:
266 if config.has_option('stgit', 'commname'):
267 name = config.get('stgit', 'commname')
268 elif 'GIT_COMMITTER_NAME' in os.environ:
269 name = os.environ['GIT_COMMITTER_NAME']
7cc615f3 270 self.__set_field('commname', name)
41a6d859
CM
271
272 def get_commemail(self):
273 return self.__get_field('commemail')
274
7cc615f3 275 def set_commemail(self, address):
4db741b1
CM
276 if not address:
277 if config.has_option('stgit', 'commemail'):
278 address = config.get('stgit', 'commemail')
279 elif 'GIT_COMMITTER_EMAIL' in os.environ:
280 address = os.environ['GIT_COMMITTER_EMAIL']
7cc615f3 281 self.__set_field('commemail', address)
41a6d859
CM
282
283
284class Series:
285 """Class including the operations on series
286 """
287 def __init__(self, name = None):
40e65b92 288 """Takes a series name as the parameter.
41a6d859 289 """
98290387
CM
290 try:
291 if name:
292 self.__name = name
293 else:
294 self.__name = git.get_head_file()
844a1640 295 self.__base_dir = git.get_base_dir()
98290387
CM
296 except git.GitException, ex:
297 raise StackException, 'GIT tree not initialised: %s' % ex
298
844a1640 299 self.__series_dir = os.path.join(self.__base_dir, 'patches',
02ac3ad2 300 self.__name)
844a1640
CM
301 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
302 self.__name)
303 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
98290387 304 self.__name)
02ac3ad2
CL
305
306 self.__applied_file = os.path.join(self.__series_dir, 'applied')
307 self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied')
308 self.__current_file = os.path.join(self.__series_dir, 'current')
309 self.__descr_file = os.path.join(self.__series_dir, 'description')
310
311 # where this series keeps its patches
312 self.__patch_dir = os.path.join(self.__series_dir, 'patches')
313 if not os.path.isdir(self.__patch_dir):
314 self.__patch_dir = self.__series_dir
41a6d859 315
844a1640
CM
316 # if no __refs_dir, create and populate it (upgrade old repositories)
317 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
318 os.makedirs(self.__refs_dir)
319 for patch in self.get_applied() + self.get_unapplied():
320 self.get_patch(patch).update_top_ref()
321
629ddd02
CM
322 def get_branch(self):
323 """Return the branch name for the Series object
324 """
325 return self.__name
326
41a6d859
CM
327 def __set_current(self, name):
328 """Sets the topmost patch
329 """
330 if name:
331 write_string(self.__current_file, name)
332 else:
333 create_empty_file(self.__current_file)
334
335 def get_patch(self, name):
336 """Return a Patch object for the given name
337 """
844a1640 338 return Patch(name, self.__patch_dir, self.__refs_dir)
41a6d859
CM
339
340 def get_current(self):
341 """Return a Patch object representing the topmost patch
342 """
343 if os.path.isfile(self.__current_file):
344 name = read_string(self.__current_file)
345 else:
346 return None
347 if name == '':
348 return None
349 else:
350 return name
351
352 def get_applied(self):
40e65b92 353 if not os.path.isfile(self.__applied_file):
a2dcde71 354 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
355 f = file(self.__applied_file)
356 names = [line.strip() for line in f.readlines()]
357 f.close()
358 return names
359
360 def get_unapplied(self):
40e65b92 361 if not os.path.isfile(self.__unapplied_file):
a2dcde71 362 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
363 f = file(self.__unapplied_file)
364 names = [line.strip() for line in f.readlines()]
365 f.close()
366 return names
367
368 def get_base_file(self):