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