Ask git for author and committer name
[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):
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
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)
64354a2d
CM
131 self.__log_ref_file = os.path.join(self.__refs_dir,
132 self.__name + '.log')
41a6d859
CM
133
134 def create(self):
135 os.mkdir(self.__dir)
136 create_empty_file(os.path.join(self.__dir, 'bottom'))
137 create_empty_file(os.path.join(self.__dir, 'top'))
138
139 def delete(self):
140 for f in os.listdir(self.__dir):
141 os.remove(os.path.join(self.__dir, f))
142 os.rmdir(self.__dir)
844a1640 143 os.remove(self.__top_ref_file)
64354a2d
CM
144 if os.path.exists(self.__log_ref_file):
145 os.remove(self.__log_ref_file)
41a6d859
CM
146
147 def get_name(self):
148 return self.__name
149
e55b53e0
CM
150 def rename(self, newname):
151 olddir = self.__dir
64354a2d
CM
152 old_top_ref_file = self.__top_ref_file
153 old_log_ref_file = self.__log_ref_file
e55b53e0 154 self.__name = newname
02ac3ad2 155 self.__dir = os.path.join(self.__series_dir, self.__name)
844a1640 156 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
64354a2d
CM
157 self.__log_ref_file = os.path.join(self.__refs_dir,
158 self.__name + '.log')
e55b53e0
CM
159
160 os.rename(olddir, self.__dir)
64354a2d
CM
161 os.rename(old_top_ref_file, self.__top_ref_file)
162 if os.path.exists(old_log_ref_file):
163 os.rename(old_log_ref_file, self.__log_ref_file)
844a1640
CM
164
165 def __update_top_ref(self, ref):
166 write_string(self.__top_ref_file, ref)
167
64354a2d
CM
168 def __update_log_ref(self, ref):
169 write_string(self.__log_ref_file, ref)
170
844a1640
CM
171 def update_top_ref(self):
172 top = self.get_top()
173 if top:
174 self.__update_top_ref(top)
e55b53e0 175
41a6d859
CM
176 def __get_field(self, name, multiline = False):
177 id_file = os.path.join(self.__dir, name)
178 if os.path.isfile(id_file):
7cc615f3
CL
179 line = read_string(id_file, multiline)
180 if line == '':
41a6d859
CM
181 return None
182 else:
7cc615f3 183 return line
41a6d859
CM
184 else:
185 return None
186
7cc615f3 187 def __set_field(self, name, value, multiline = False):
41a6d859 188 fname = os.path.join(self.__dir, name)
7cc615f3
CL
189 if value and value != '':
190 write_string(fname, value, multiline)
41a6d859
CM
191 elif os.path.isfile(fname):
192 os.remove(fname)
193
54b09584
PBG
194 def get_old_bottom(self):
195 return self.__get_field('bottom.old')
196
41a6d859
CM
197 def get_bottom(self):
198 return self.__get_field('bottom')
199
7cc615f3 200 def set_bottom(self, value, backup = False):
41a6d859 201 if backup:
a5bbc44d 202 curr = self.__get_field('bottom')
f80bef49 203 self.__set_field('bottom.old', curr)
7cc615f3 204 self.__set_field('bottom', value)
41a6d859 205
54b09584
PBG
206 def get_old_top(self):
207 return self.__get_field('top.old')
208
41a6d859
CM
209 def get_top(self):
210 return self.__get_field('top')
211
7cc615f3 212 def set_top(self, value, backup = False):
41a6d859 213 if backup:
a5bbc44d 214 curr = self.__get_field('top')
f80bef49 215 self.__set_field('top.old', curr)
7cc615f3 216 self.__set_field('top', value)
844a1640 217 self.__update_top_ref(value)
41a6d859
CM
218
219 def restore_old_boundaries(self):
220 bottom = self.__get_field('bottom.old')
221 top = self.__get_field('top.old')
222
223 if top and bottom:
224 self.__set_field('bottom', bottom)
225 self.__set_field('top', top)
844a1640 226 self.__update_top_ref(top)
a5bbc44d 227 return True
41a6d859 228 else:
a5bbc44d 229 return False
41a6d859
CM
230
231 def get_description(self):
232 return self.__get_field('description', True)
233
7cc615f3
CL
234 def set_description(self, line):
235 self.__set_field('description', line, True)
41a6d859
CM
236
237 def get_authname(self):
238 return self.__get_field('authname')
239
7cc615f3 240 def set_authname(self, name):
9e3f506f 241 self.__set_field('authname', name or git.author().name)
41a6d859
CM
242
243 def get_authemail(self):
244 return self.__get_field('authemail')
245
9e3f506f
KH
246 def set_authemail(self, email):
247 self.__set_field('authemail', email or git.author().email)
41a6d859
CM
248
249 def get_authdate(self):
250 return self.__get_field('authdate')
251
4db741b1 252 def set_authdate(self, date):
9e3f506f 253 self.__set_field('authdate', date or git.author().date)
41a6d859
CM
254
255 def get_commname(self):
256 return self.__get_field('commname')
257
7cc615f3 258 def set_commname(self, name):
9e3f506f 259 self.__set_field('commname', name or git.committer().name)
41a6d859
CM
260
261 def get_commemail(self):
262 return self.__get_field('commemail')
263
9e3f506f
KH
264 def set_commemail(self, email):
265 self.__set_field('commemail', email or git.committer().email)
41a6d859 266
64354a2d
CM
267 def get_log(self):
268 return self.__get_field('log')
269
270 def set_log(self, value, backup = False):
271 self.__set_field('log', value)
272 self.__update_log_ref(value)
273
41a6d859
CM
274
275class Series:
276 """Class including the operations on series
277 """
278 def __init__(self, name = None):
40e65b92 279 """Takes a series name as the parameter.
41a6d859 280 """
98290387
CM
281 try:
282 if name:
283 self.__name = name
284 else:
285 self.__name = git.get_head_file()
170f576b 286 self.__base_dir = basedir.get()
98290387
CM
287 except git.GitException, ex:
288 raise StackException, 'GIT tree not initialised: %s' % ex
289
844a1640 290 self.__series_dir = os.path.join(self.__base_dir, 'patches',
02ac3ad2 291 self.__name)
844a1640
CM
292 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
293 self.__name)
294 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
98290387 295 self.__name)
02ac3ad2
CL
296
297 self.__applied_file = os.path.join(self.__series_dir, 'applied')
298 self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied')
299 self.__current_file = os.path.join(self.__series_dir, 'current')
300 self.__descr_file = os.path.join(self.__series_dir, 'description')
301
302 # where this series keeps its patches
303 self.__patch_dir = os.path.join(self.__series_dir, 'patches')
304 if not os.path.isdir(self.__patch_dir):
305 self.__patch_dir = self.__series_dir
41a6d859 306
844a1640
CM
307 # if no __refs_dir, create and populate it (upgrade old repositories)
308 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
309 os.makedirs(self.__refs_dir)
310 for patch in self.get_applied() + self.get_unapplied():
311 self.get_patch(patch).update_top_ref()
312
ac50371b
CM
313 # trash directory
314 self.__trash_dir = os.path.join(self.__series_dir, 'trash')
315 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
316 os.makedirs(self.__trash_dir)
317
629ddd02
CM
318 def get_branch(self):
319 """Return the branch name for the Series object
320 """
321 return self.__name
322
41a6d859
CM
323 def __set_current(self, name):
324 """Sets the topmost patch
325 """
326 if name:
327 write_string(self.__current_file, name)
328 else:
329 create_empty_file(self.__current_file)
330
331 def get_patch(self, name):
332 """Return a Patch object for the given name
333 """
844a1640 334 return Patch(name, self.__patch_dir, self.__refs_dir)
41a6d859 335
4d0ba818
KH
336 def get_current_patch(self):
337 """Return a Patch object representing the topmost patch, or
338 None if there is no such patch."""
339 crt = self.get_current()
340 if not crt:
341 return None
342 return Patch(crt, self.__patch_dir, self.__refs_dir)
343
41a6d859 344 def get_current(self):
4d0ba818
KH
345 """Return the name of the topmost patch, or None if there is
346 no such patch."""
41a6d859
CM
347 if os.path.isfile(self.__current_file):
348 name = read_string(self.__current_file)
349 else:
350 return None
351 if name == '':
352 return None
353 else:
354 return name
355
356 def get_applied(self):
40e65b92 357 if not os.path.isfile(self.__applied_file):
a2dcde71 358 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
359 f = file(self.__applied_file)
360 names = [line.strip() for line in f.readlines()]
361 f.close()
362 return names
363
364 def get_unapplied(self):
40e65b92 365 if not os.path.isfile(self.__unapplied_file):
a2dcde71 366 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
367 f = file(self.__unapplied_file)
368 names = [line.strip() for line in f.readlines()]
369 f.close()
370 return names
371
372 def get_base_file(self):