Add --binary flag to commands that generate diffs
[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
c1e4d7e0 21import sys, os, re
41a6d859
CM
22
23from stgit.utils import *
1f3bb017 24from stgit import git, basedir, templates
41a6d859 25from stgit.config import config
8fce9909 26from shutil import copyfile
41a6d859
CM
27
28
29# stack exception class
30class StackException(Exception):
31 pass
32
6ad48e48
PBG
33class FilterUntil:
34 def __init__(self):
35 self.should_print = True
36 def __call__(self, x, until_test, prefix):
37 if until_test(x):
38 self.should_print = False
39 if self.should_print:
40 return x[0:len(prefix)] != prefix
41 return False
42
41a6d859
CM
43#
44# Functions
45#
46__comment_prefix = 'STG:'
6ad48e48 47__patch_prefix = 'STG_PATCH:'
41a6d859
CM
48
49def __clean_comments(f):
50 """Removes lines marked for status in a commit file
51 """
52 f.seek(0)
53
54 # remove status-prefixed lines
6ad48e48
PBG
55 lines = f.readlines()
56
57 patch_filter = FilterUntil()
58 until_test = lambda t: t == (__patch_prefix + '\n')
59 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
60
41a6d859
CM
61 # remove empty lines at the end
62 while len(lines) != 0 and lines[-1] == '\n':
63 del lines[-1]
64
65 f.seek(0); f.truncate()
66 f.writelines(lines)
67
7cc615f3 68def edit_file(series, line, comment, show_patch = True):
bd427e46 69 fname = '.stgitmsg.txt'
1f3bb017 70 tmpl = templates.get_template('patchdescr.tmpl')
41a6d859
CM
71
72 f = file(fname, 'w+')
7cc615f3
CL
73 if line:
74 print >> f, line
1f3bb017
CM
75 elif tmpl:
76 print >> f, tmpl,
41a6d859
CM
77 else:
78 print >> f
79 print >> f, __comment_prefix, comment
80 print >> f, __comment_prefix, \
81 'Lines prefixed with "%s" will be automatically removed.' \
82 % __comment_prefix
83 print >> f, __comment_prefix, \
84 'Trailing empty lines will be automatically removed.'
6ad48e48
PBG
85
86 if show_patch:
87 print >> f, __patch_prefix
88 # series.get_patch(series.get_current()).get_top()
89 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
90
91 #Vim modeline must be near the end.
b83e37e0 92 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
41a6d859
CM
93 f.close()
94
83bb4e4c 95 call_editor(fname)
41a6d859
CM
96
97 f = file(fname, 'r+')
98
99 __clean_comments(f)
100 f.seek(0)
7cc615f3 101 result = f.read()
41a6d859
CM
102
103 f.close()
104 os.remove(fname)
105
7cc615f3 106 return result
41a6d859
CM
107
108#
109# Classes
110#
111
8fe7e9f0
YD
112class StgitObject:
113 """An object with stgit-like properties stored as files in a directory
114 """
115 def _set_dir(self, dir):
116 self.__dir = dir
117 def _dir(self):
118 return self.__dir
119
120 def create_empty_field(self, name):
121 create_empty_file(os.path.join(self.__dir, name))
122
123 def _get_field(self, name, multiline = False):
124 id_file = os.path.join(self.__dir, name)
125 if os.path.isfile(id_file):
126 line = read_string(id_file, multiline)
127 if line == '':
128 return None
129 else:
130 return line
131 else:
132 return None
133
134 def _set_field(self, name, value, multiline = False):
135 fname = os.path.join(self.__dir, name)
136 if value and value != '':
137 write_string(fname, value, multiline)
138 elif os.path.isfile(fname):
139 os.remove(fname)
140
141
142class Patch(StgitObject):
41a6d859
CM
143 """Basic patch implementation
144 """
844a1640 145 def __init__(self, name, series_dir, refs_dir):
02ac3ad2 146 self.__series_dir = series_dir
41a6d859 147 self.__name = name
8fe7e9f0 148 self._set_dir(os.path.join(self.__series_dir, self.__name))
844a1640
CM
149 self.__refs_dir = refs_dir
150 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
64354a2d
CM
151 self.__log_ref_file = os.path.join(self.__refs_dir,
152 self.__name + '.log')
41a6d859
CM
153
154 def create(self):
8fe7e9f0
YD
155 os.mkdir(self._dir())
156 self.create_empty_field('bottom')
157 self.create_empty_field('top')
41a6d859
CM
158
159 def delete(self):
8fe7e9f0
YD
160 for f in os.listdir(self._dir()):
161 os.remove(os.path.join(self._dir(), f))
162 os.rmdir(self._dir())
844a1640 163 os.remove(self.__top_ref_file)
64354a2d
CM
164 if os.path.exists(self.__log_ref_file):
165 os.remove(self.__log_ref_file)
41a6d859
CM
166
167 def get_name(self):
168 return self.__name
169
e55b53e0 170 def rename(self, newname):
8fe7e9f0 171 olddir = self._dir()
64354a2d
CM
172 old_top_ref_file = self.__top_ref_file
173 old_log_ref_file = self.__log_ref_file
e55b53e0 174 self.__name = newname
8fe7e9f0 175 self._set_dir(os.path.join(self.__series_dir, self.__name))
844a1640 176 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
64354a2d
CM
177 self.__log_ref_file = os.path.join(self.__refs_dir,
178 self.__name + '.log')
e55b53e0 179
8fe7e9f0 180 os.rename(olddir, self._dir())
64354a2d
CM
181 os.rename(old_top_ref_file, self.__top_ref_file)
182 if os.path.exists(old_log_ref_file):
183 os.rename(old_log_ref_file, self.__log_ref_file)
844a1640
CM
184
185 def __update_top_ref(self, ref):
186 write_string(self.__top_ref_file, ref)
187
64354a2d
CM
188 def __update_log_ref(self, ref):
189 write_string(self.__log_ref_file, ref)
190
844a1640
CM
191 def update_top_ref(self):
192 top = self.get_top()
193 if top:
194 self.__update_top_ref(top)
e55b53e0 195
54b09584 196 def get_old_bottom(self):
8fe7e9f0 197 return self._get_field('bottom.old')
54b09584 198
41a6d859 199 def get_bottom(self):
8fe7e9f0 200 return self._get_field('bottom')
41a6d859 201
7cc615f3 202 def set_bottom(self, value, backup = False):
41a6d859 203 if backup:
8fe7e9f0
YD
204 curr = self._get_field('bottom')
205 self._set_field('bottom.old', curr)
206 self._set_field('bottom', value)
41a6d859 207
54b09584 208 def get_old_top(self):
8fe7e9f0 209 return self._get_field('top.old')
54b09584 210
41a6d859 211 def get_top(self):
8fe7e9f0 212 return self._get_field('top')
41a6d859 213
7cc615f3 214 def set_top(self, value, backup = False):
41a6d859 215 if backup:
8fe7e9f0
YD
216 curr = self._get_field('top')
217 self._set_field('top.old', curr)
218 self._set_field('top', value)
844a1640 219 self.__update_top_ref(value)
41a6d859
CM
220
221 def restore_old_boundaries(self):
8fe7e9f0
YD
222 bottom = self._get_field('bottom.old')
223 top = self._get_field('top.old')
41a6d859
CM
224
225 if top and bottom:
8fe7e9f0
YD
226 self._set_field('bottom', bottom)
227 self._set_field('top', top)
844a1640 228 self.__update_top_ref(top)
a5bbc44d 229 return True
41a6d859 230 else:
a5bbc44d 231 return False
41a6d859
CM
232
233 def get_description(self):
8fe7e9f0 234 return self._get_field('description', True)
41a6d859 235
7cc615f3 236 def set_description(self, line):
8fe7e9f0 237 self._set_field('description', line, True)
41a6d859
CM
238
239 def get_authname(self):
8fe7e9f0 240 return self._get_field('authname')
41a6d859 241
7cc615f3 242 def set_authname(self, name):
8fe7e9f0 243 self._set_field('authname', name or git.author().name)
41a6d859
CM
244
245 def get_authemail(self):
8fe7e9f0 246 return self._get_field('authemail')
41a6d859 247
9e3f506f 248 def set_authemail(self, email):
8fe7e9f0 249 self._set_field('authemail', email or git.author().email)
41a6d859
CM
250
251 def get_authdate(self):
8fe7e9f0 252 return self._get_field('authdate')
41a6d859 253
4db741b1 254 def set_authdate(self, date):
8fe7e9f0 255 self._set_field('authdate', date or git.author().date)
41a6d859
CM
256
257 def get_commname(self):
8fe7e9f0 258 return self._get_field('commname')
41a6d859 259
7cc615f3 260 def set_commname(self, name):
8fe7e9f0 261 self._set_field('commname', name or git.committer().name)
41a6d859
CM
262
263 def get_commemail(self):
8fe7e9f0 264 return self._get_field('commemail')
41a6d859 265
9e3f506f 266 def set_commemail(self, email):
8fe7e9f0 267 self._set_field('commemail', email or git.committer().email)
41a6d859 268
64354a2d 269 def get_log(self):
8fe7e9f0 270 return self._get_field('log')
64354a2d
CM
271
272 def set_log(self, value, backup = False):
8fe7e9f0 273 self._set_field('log', value)
64354a2d
CM
274 self.__update_log_ref(value)
275
598e9d3f
KH
276# The current StGIT metadata format version.
277FORMAT_VERSION = 2
278
279def format_version_key(branch):
280 return 'branch.%s.stgitformatversion' % branch
281
282def update_to_current_format_version(branch, git_dir):
283 """Update a potentially older StGIT directory structure to the
284 latest version. Note: This function should depend as little as
285 possible on external functions that may change during a format
286 version bump, since it must remain able to process older formats."""
287
288 branch_dir = os.path.join(git_dir, 'patches', branch)
289 def get_format_version():
290 """Return the integer format version number, or None if the
291 branch doesn't have any StGIT metadata at all, of any version."""
292 fv = config.get(format_version_key(branch))
293 if fv:
294 # Great, there's an explicitly recorded format version
295 # number, which means that the branch is initialized and
296 # of that exact version.
297 return int(fv)
298 elif os.path.isdir(os.path.join(branch_dir, 'patches')):
299 # There's a .git/patches/<branch>/patches dirctory, which
300 # means this is an initialized version 1 branch.
301 return 1
302 elif os.path.isdir(branch_dir):
303 # There's a .git/patches/<branch> directory, which means
304 # this is an initialized version 0 branch.
305 return 0
306 else:
307 # The branch doesn't seem to be initialized at all.
308 return None
309 def set_format_version(v):
310 config.set(format_version_key(branch), '%d' % v)
311 def mkdir(d):
312 if not os.path.isdir(d):
313 os.makedirs(d)
314 def rm(f):
315 if os.path.exists(f):
316 os.remove(f)
317
318 # Update 0 -> 1.
319 if get_format_version() == 0:
320 mkdir(os.path.join(branch_dir, 'trash'))
321 patch_dir = os.path.join(branch_dir, 'patches')
322 mkdir(patch_dir)
323 refs_dir = os.path.join(git_dir, 'refs', 'patches', branch)
324 mkdir(refs_dir)
325 for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
326 + file(os.path.join(branch_dir, 'applied')).readlines()):
327 patch = patch.strip()
328 os.rename(os.path.join(branch_dir, patch),
329 os.path.join(patch_dir, patch))
330 Patch(patch, patch_dir, refs_dir).update_top_ref()
331 set_format_version(1)
332
333 # Update 1 -> 2.
334 if get_format_version() == 1:
335 desc_file = os.path.join(branch_dir, 'description')
336 if os.path.isfile(desc_file):
337 desc = read_string(desc_file)
338 if desc:
339 config.set('branch.%s.description' % branch, desc)
340 rm(desc_file)
341 rm(os.path.join(branch_dir, 'current'))
342 rm(os.path.join(git_dir, 'refs', 'bases', branch))
343 set_format_version(2)
344
345 # Make sure we're at the latest version.
346 if not get_format_version() in [None, FORMAT_VERSION]:
347 raise StackException('Branch %s is at format version %d, expected %d'
348 % (branch, get_format_version(), FORMAT_VERSION))
41a6d859 349
8fe7e9f0 350class Series(StgitObject):
41a6d859
CM
351 """Class including the operations on series
352 """
353 def __init__(self, name = None):
40e65b92 354 """Takes a series name as the parameter.
41a6d859 355 """
98290387
CM
356 try:
357 if name:
358 self.__name = name
359 else:
360 self.__name = git.get_head_file()
170f576b 361 self.__base_dir = basedir.get()
98290387
CM
362 except git.GitException, ex:
363 raise StackException, 'GIT tree not initialised: %s' % ex
364
8fe7e9f0 365 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
598e9d3f
KH
366
367 # Update the branch to the latest format version if it is
368 # initialized, but don't touch it if it isn't.
369 update_to_current_format_version(self.__name, self.__base_dir)
370
844a1640
CM
371 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
372 self.__name)
02ac3ad2 373
8fe7e9f0
YD
374 self.__applied_file = os.path.join(self._dir(), 'applied')
375 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
841c7b2a 376 self.__hidden_file = os.path.join(self._dir(), 'hidden')
02ac3ad2
CL
377
378 # where this series keeps its patches
8fe7e9f0 379 self.__patch_dir = os.path.join(self._dir(), 'patches')
844a1640 380
ac50371b 381 # trash directory
8fe7e9f0 382 self.__trash_dir = os.path.join(self._dir(), 'trash')
ac50371b 383
c1e4d7e0
CM
384 def __patch_name_valid(self, name):
385 """Raise an exception if the patch name is not valid.
386 """
387 if not name or re.search('[^\w.-]', name):
388 raise StackException, 'Invalid patch name: "%s"' % name
389
629ddd02
CM
390 def get_branch(self):
391 """Return the branch name for the Series object
392 """
393 return self.__name
394
41a6d859
CM
395 def get_patch(self, name):
396 """Return a Patch object for the given name
397 """
844a1640 398 return Patch(name, self.__patch_dir, self.__refs_dir)
41a6d859 399
4d0ba818
KH
400 def get_current_patch(self):
401 """Return a Patch object representing the topmost patch, or
402 None if there is no such patch."""
403 crt = self.get_current()
404 if not crt:
405 return None
406 return Patch(crt, self.__patch_dir, self.__refs_dir)
407
41a6d859 408 def get_current(self):
4d0ba818
KH
409 """Return the name of the topmost patch, or None if there is
410 no such patch."""
532cdf94
KH
411 try:
412 applied = self.get_applied()
413 except StackException:
414 # No "applied" file: branch is not initialized.
415 return None
416 try:
417 return applied[-1]
418 except IndexError:
419 # No patches applied.
41a6d859 420 return None
41a6d859
CM
421
422 def get_applied(self):
40e65b92 423 if not os.path.isfile(self.__applied_file):
a2dcde71 424 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
425 f = file(self.__applied_file)
426 names = [line.strip() for line in f.readlines()]
427 f.close()
428 return names
429
430 def get_unapplied(self):
40e65b92 431 if not os.path.isfile(self.__unapplied_file):
a2dcde71 432 raise StackException, 'Branch "%s" not initialised' % self.__name
41a6d859
CM
433 f = file(self.__unapplied_file)
434 names = [line.strip() for line in f.readlines()]
435 f.close()
436 return names
437
841c7b2a
CM
438 def get_hidden(self):
439 if not os.path.isfile(self.__hidden_file):
440 return []
441 f = file(self.__hidden_file)
442 names = [line.strip() for line in f.readlines()]
443 f.close()
444 return names
445
ba66e579 446 def get_base(self):
16d69115
KH
447 # Return the parent of the bottommost patch, if there is one.
448 if os.path.isfile(self.__applied_file):
449 bottommost = file(self.__applied_file).readline().strip()
450 if bottommost:
451 return self.get_patch(bottommost).get_bottom()
452 # No bottommost patch, so just return HEAD
453 return git.get_head()
ba66e579 454
e078133e
CM
455 def get_head(self):
456 """Return the head of the branch
457 """
458 crt = self.get_current_patch()
459 if crt:
460 return crt.get_top()
461 else:
462 return self.get_base()
463
0b4b9499 464 def get_protected(self):
8fe7e9f0 465 return os.path.isfile(os.path.join(self._dir(), 'protected'))
0b4b9499
CL
466
467 def protect(self):
8fe7e9f0 468 protect_file = os.path.join(self._dir(), 'protected')
0b4b9499
CL
469 if not os.path.isfile(protect_file):
470 create_empty_file(protect_file)
471
472 def unprotect(self):
8fe7e9f0 473 protect_file = os.path.join(self._dir(), 'protected')
0b4b9499
CL
474 if os.path.isfile(protect_file):
475 os.remove(protect_file)
476
4975762e
KH
477 def __branch_descr(self):
478 return 'branch.%s.description' % self.get_branch()
479
c1fe1f99 480 def get_description(self):
598e9d3f 481 return config.get(self.__branch_descr()) or ''
8fe7e9f0
YD
482
483 def set_description(self, line):
4975762e
KH
484 if line:
485 config.set(self.__branch_descr(), line)
486 else:
487 config.unset(self.__branch_descr())
c1fe1f99 488
254d99f8 489 def get_parent_remote(self):
f72ad3d6
YD
490 value = config.get('branch.%s.remote' % self.__name)
491 if value:
492 return value
493 elif 'origin' in git.remotes_list():
4646e7a3
YD
494 print 'Notice: no parent remote declared for stack "%s", ' \
495 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
496 'and "branch.%s.merge" with "git repo-config".' \
497 % (self.__name, self.__name, self.__name)
f72ad3d6
YD
498 return 'origin'
499 else:
500 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
254d99f8
YD
501
502 def __set_parent_remote(self, remote):
503 value = config.set('branch.%s.remote' % self.__name, remote)
504
8866feda 505 def get_parent_branch(self):
4646e7a3 506 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
8866feda
YD
507 if value:
508 return value
509 elif git.rev_parse('heads/origin'):
4646e7a3
YD
510 print 'Notice: no parent branch declared for stack "%s", ' \
511 'defaulting to "heads/origin". Consider setting ' \
512 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
c0167829 513 % (self.__name, self.__name)
8866feda
YD
514 return 'heads/origin'
515 else:
516 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
517
518 def __set_parent_branch(self, name):
4646e7a3
YD
519 if config.get('branch.%s.remote' % self.__name):
520 # Never set merge if remote is not set to avoid
521 # possibly-erroneous lookups into 'origin'
522 config.set('branch.%s.merge' % self.__name, name)
523 config.set('branch.%s.stgit.parentbranch' % self.__name, name)
8866feda
YD
524
525 def set_parent(self, remote, localbranch):
526 if localbranch:
1fa161d6 527 self.__set_parent_remote(remote)
8866feda 528 self.__set_parent_branch(localbranch)
4646e7a3
YD
529 # We'll enforce this later
530# else:
531# raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
8866feda 532
41a6d859 533 def __patch_is_current(self, patch):
8fe7e9f0 534 return patch.get_name() == self.get_current()
41a6d859 535
ed0350be 536 def patch_applied(self, name):
41a6d859
CM
537 """Return true if the patch exists in the applied list
538 """
539 return name in self.get_applied()
540
ed0350be 541 def patch_unapplied(self, name):
41a6d859
CM
542 """Return true if the patch exists in the unapplied list
543 """
544 return name in self.get_unapplied()
545
841c7b2a
CM
546 def patch_hidden(self, name):
547 """Return true if the patch is hidden.
548 """
549 return name in self.get_hidden()
550
4d0ba818
KH
551 def patch_exists(self, name):
552 """Return true if there is a patch with the given name, false
553 otherwise."""
ed0350be 554 return self.patch_applied(name) or self.patch_unapplied(name)
4d0ba818 555
41a6d859
CM
556 def head_top_equal(self):
557 """Return true if the head and the top are the same
558 """
4d0ba818 559 crt = self.get_current_patch()
41a6d859
CM
560 if not crt:
561 # we don't care, no patches applied
562 return True
4d0ba818 563 return git.get_head() == crt.get_top()
41a6d859 564
2d00440c
CL
565 def is_initialised(self):
566 """Checks if series is already initialised
567 """
598e9d3f 568 return bool(config.get(format_version_key(self.get_branch())))
2d00440c 569
8866feda 570 def init(self, create_at=False, parent_remote=None, parent_branch=None):
41a6d859
CM
571 """Initialises the stgit series
572 """
598e9d3f
KH
573 if self.is_initialised():
574 raise StackException, '%s already initialized' % self.get_branch()
575 for d in [self._dir(), self.__refs_dir]:
576 if os.path.exists(d):
577 raise StackException, '%s already exists' % d
fe847176 578
a22a62b6
YD
579 if (create_at!=False):
580 git.create_branch(self.__name, create_at)
581
41a6d859
CM
582 os.makedirs(self.__patch_dir)
583
8866feda 584 self.set_parent(parent_remote, parent_branch)
41a6d859 585
8fe7e9f0
YD
586 self.create_empty_field('applied')
587 self.create_empty_field('unapplied')
844a1640 588 os.makedirs(self.__refs_dir)
b6c95ada 589 self._set_field('orig-base', git.get_head())
41a6d859 590
598e9d3f 591 config.set(format_version_key(self.get_branch()), str(FORMAT_VERSION))
bad9dcfc 592
660ba985
CL
593 def rename(self, to_name):
594 """Renames a series
595 """
596 to_stack = Series(to_name)
84bf6268
CL
597
598 if to_stack.is_initialised():
599 raise StackException, '"%s" already exists' % to_stack.get_branch()
660ba985
CL
600
601 git.rename_branch(self.__name, to_name)
602
8fe7e9f0 603 if os.path.isdir(self._dir()):