Automatically generate patch names for uncommit
[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 *
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):
41a6d859 68 fname = '.stgit.msg'
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
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')
f80bef49 191 self.__set_field('bottom.old', curr)
7cc615f3 192 self.__set_field('bottom', value)
41a6d859 193
54b09584
PBG
194 def get_old_top(self):
195 return self.__get_field('top.old')
196
41a6d859
CM
197 def get_top(self):
198 return self.__get_field('top')
199
7cc615f3 200 def set_top(self, value, backup = False):
41a6d859 201 if backup:
a5bbc44d 202 curr = self.__get_field('top')
f80bef49 203 self.__set_field('top.old', curr)
7cc615f3 204 self.__set_field('top', value)
844a1640 205 self.__update_top_ref(value)
41a6d859
CM
206
207 def restore_old_boundaries(self):
208 bottom = self.__get_field('bottom.old')
209 top = self.__get_field('top.old')
210
211 if top and bottom:
212 self.__set_field('bottom', bottom)
213 self.__set_field('top', top)
844a1640 214 self.__update_top_ref(top)
a5bbc44d 215 return True
41a6d859 216 else:
a5bbc44d 217 return False
41a6d859
CM
218
219 def get_description(self):
220 return self.__get_field('description', True)
221
7cc615f3
CL
222 def set_description(self, line):
223 self.__set_field('description', line, True)
41a6d859
CM
224
225 def get_authname(self):
226 return self.__get_field('authname')
227
7cc615f3 228 def set_authname(self, name):
4db741b1
CM
229 if not name:
230 if config.has_option('stgit', 'authname'):
231 name = config.get('stgit', 'authname')
232 elif 'GIT_AUTHOR_NAME' in os.environ:
233 name = os.environ['GIT_AUTHOR_NAME']
7cc615f3 234 self.__set_field('authname', name)
41a6d859
CM
235
236 def get_authemail(self):
237 return self.__get_field('authemail')
238
7cc615f3 239 def set_authemail(self, address):
4db741b1
CM
240 if not address:
241 if config.has_option('stgit', 'authemail'):
242 address = config.get('stgit', 'authemail')
243 elif 'GIT_AUTHOR_EMAIL' in os.environ:
244 address = os.environ['GIT_AUTHOR_EMAIL']
7cc615f3 245 self.__set_field('authemail', address)
41a6d859
CM
246
247 def get_authdate(self):
248 return self.__get_field('authdate')
249
4db741b1
CM
250 def set_authdate(self, date):
251 if not date and 'GIT_AUTHOR_DATE' in os.environ:
252 date = os.environ['GIT_AUTHOR_DATE']
253 self.__set_field('authdate', date)
41a6d859
CM
254
255 def get_commname(self):
256 return self.__get_field('commname')
257
7cc615f3 258 def set_commname(self, name):
4db741b1
CM
259 if not name:
260 if config.has_option('stgit', 'commname'):
261 name = config.get('stgit', 'commname')
262 elif 'GIT_COMMITTER_NAME' in os.environ:
263 name = os.environ['GIT_COMMITTER_NAME']
7cc615f3 264 self.__set_field('commname', name)
41a6d859
CM
265
266 def get_commemail(self):
267 return self.__get_field('commemail')
268
7cc615f3 269 def set_commemail(self, address):
4db741b1
CM
270 if not address:
271 if config.has_option('stgit', 'commemail'):
272 address = config.get('stgit', 'commemail')
273 elif 'GIT_COMMITTER_EMAIL' in os.environ:
274 address = os.environ['GIT_COMMITTER_EMAIL']
7cc615f3 275 self.__set_field('commemail', address)
41a6d859
CM
276
277
278class Series:
279 """Class including the operations on series
280 """
281 def __init__(self, name = None):
40e65b92 282 """Takes a series name as the parameter.
41a6d859 283 """
98290387
CM
284 try:
285 if name:
286 self.__name = name
287 else:
288 self.__name = git.get_head_file()
170f576b 289 self.__base_dir = basedir.get()
98290387
CM
290 except git.GitException, ex:
291 raise StackException, 'GIT tree not initialised: %s' % ex
292
844a1640 293 self.__series_dir = os.path.join(self.__base_dir, 'patches',
02ac3ad2 294 self.__name)
844a1640
CM
295 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
296 self.__name)
297 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
98290387 298 self.__name)
02ac3ad2
CL
299
300 self.__applied_file = os.path.join(self.__series_dir, 'applied')
301 self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied')
302 self.__current_file = os.path.join(self.__series_dir, 'current')
303 self.__descr_file = os.path.join(self.__series_dir, 'description')
304
305 # where this series keeps its patches
306 self.__patch_dir = os.path.join(self.__series_dir, 'patches')
307 if not os.path.isdir(self.__patch_dir):
308 self.__patch_dir = self.__series_dir
41a6d859 309
844a1640
CM
310 # if no __refs_dir, create and populate it (upgrade old repositories)
311 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
312 os.makedirs(self.__refs_dir)
313 for patch in self.get_applied() + self.get_unapplied():
314 self.get_patch(patch).update_top_ref()
315
629ddd02
CM
316 def get_branch(self):
317 """Return the branch name for the Series object
318 """
319 return self.__name
320
41a6d859
CM
321 def __set_current(self, name):
322 """Sets the topmost patch
323 """
324 if name:
325 write_string(self.__current_file, name)
326 else:
327 create_empty_file(self.__current_file)
328
329 def get_patch(self, name):
330 """Return a Patch object for the given name
331 """
844a1640 332 return Patch(name, self.__patch_dir, self.__refs_dir)
41a6d859
CM
333
334 def get_current(self):
335 """Return a Patch object representing the topmost patch
336 """
337 if os.path.isfile(self.__current_file):
338 name = read_string(self.__current_file)
339 else:
340 return None
341 if name == '':
342 return None
343 else:
344 return name
345
346 def get_applied(self):
40e65b92 347 if not os.path.isfile(self.__applied_file):
a2dcde71 348 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
349 f = file(self.__applied_file)
350 names = [line.strip() for line in f.readlines()]
351 f.close()
352 return names
353
354 def get_unapplied(self):
40e65b92 355 if not os.path.isfile(self.__unapplied_file):
a2dcde71 356 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
357 f = file(self.__unapplied_file)
358 names = [line.strip() for line in f.readlines()]
359 f.close()
360 return names
361
362 def get_base_file(self):