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