Factorize editor handling.
[stgit.git] / stgit / stack.py
blobfeb77e343341d3da9c03a60c60cd10e1c07ef608
1 """Basic quilt-like functionality
2 """
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
21 import sys, os, re
23 from stgit.utils import *
24 from stgit import git, basedir, templates
25 from stgit.config import config
28 # stack exception class
29 class StackException(Exception):
30 pass
32 class 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
43 # Functions
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
48 def __clean_comments(f):
49 """Removes lines marked for status in a commit file
50 """
51 f.seek(0)
53 # remove status-prefixed lines
54 lines = f.readlines()
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)]
60 # remove empty lines at the end
61 while len(lines) != 0 and lines[-1] == '\n':
62 del lines[-1]
64 f.seek(0); f.truncate()
65 f.writelines(lines)
67 def edit_file(series, line, comment, show_patch = True):
68 fname = '.stgitmsg.txt'
69 tmpl = templates.get_template('patchdescr.tmpl')
71 f = file(fname, 'w+')
72 if line:
73 print >> f, line
74 elif tmpl:
75 print >> f, tmpl,
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.'
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)
90 #Vim modeline must be near the end.
91 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
92 f.close()
94 call_editor(fname)
96 f = file(fname, 'r+')
98 __clean_comments(f)
99 f.seek(0)
100 result = f.read()
102 f.close()
103 os.remove(fname)
105 return result
108 # Classes
111 class StgitObject:
112 """An object with stgit-like properties stored as files in a directory
114 def _set_dir(self, dir):
115 self.__dir = dir
116 def _dir(self):
117 return self.__dir
119 def create_empty_field(self, name):
120 create_empty_file(os.path.join(self.__dir, name))
122 def _get_field(self, name, multiline = False):
123 id_file = os.path.join(self.__dir, name)
124 if os.path.isfile(id_file):
125 line = read_string(id_file, multiline)
126 if line == '':
127 return None
128 else:
129 return line
130 else:
131 return None
133 def _set_field(self, name, value, multiline = False):
134 fname = os.path.join(self.__dir, name)
135 if value and value != '':
136 write_string(fname, value, multiline)
137 elif os.path.isfile(fname):
138 os.remove(fname)
141 class Patch(StgitObject):
142 """Basic patch implementation
144 def __init__(self, name, series_dir, refs_dir):
145 self.__series_dir = series_dir
146 self.__name = name
147 self._set_dir(os.path.join(self.__series_dir, self.__name))
148 self.__refs_dir = refs_dir
149 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
150 self.__log_ref_file = os.path.join(self.__refs_dir,
151 self.__name + '.log')
153 def create(self):
154 os.mkdir(self._dir())
155 self.create_empty_field('bottom')
156 self.create_empty_field('top')
158 def delete(self):
159 for f in os.listdir(self._dir()):
160 os.remove(os.path.join(self._dir(), f))
161 os.rmdir(self._dir())
162 os.remove(self.__top_ref_file)
163 if os.path.exists(self.__log_ref_file):
164 os.remove(self.__log_ref_file)
166 def get_name(self):
167 return self.__name
169 def rename(self, newname):
170 olddir = self._dir()
171 old_top_ref_file = self.__top_ref_file
172 old_log_ref_file = self.__log_ref_file
173 self.__name = newname
174 self._set_dir(os.path.join(self.__series_dir, self.__name))
175 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
176 self.__log_ref_file = os.path.join(self.__refs_dir,
177 self.__name + '.log')
179 os.rename(olddir, self._dir())
180 os.rename(old_top_ref_file, self.__top_ref_file)
181 if os.path.exists(old_log_ref_file):
182 os.rename(old_log_ref_file, self.__log_ref_file)
184 def __update_top_ref(self, ref):
185 write_string(self.__top_ref_file, ref)
187 def __update_log_ref(self, ref):
188 write_string(self.__log_ref_file, ref)
190 def update_top_ref(self):
191 top = self.get_top()
192 if top:
193 self.__update_top_ref(top)
195 def get_old_bottom(self):
196 return self._get_field('bottom.old')
198 def get_bottom(self):
199 return self._get_field('bottom')
201 def set_bottom(self, value, backup = False):
202 if backup:
203 curr = self._get_field('bottom')
204 self._set_field('bottom.old', curr)
205 self._set_field('bottom', value)
207 def get_old_top(self):
208 return self._get_field('top.old')
210 def get_top(self):
211 return self._get_field('top')
213 def set_top(self, value, backup = False):
214 if backup:
215 curr = self._get_field('top')
216 self._set_field('top.old', curr)
217 self._set_field('top', value)
218 self.__update_top_ref(value)
220 def restore_old_boundaries(self):
221 bottom = self._get_field('bottom.old')
222 top = self._get_field('top.old')
224 if top and bottom:
225 self._set_field('bottom', bottom)
226 self._set_field('top', top)
227 self.__update_top_ref(top)
228 return True
229 else:
230 return False
232 def get_description(self):
233 return self._get_field('description', True)
235 def set_description(self, line):
236 self._set_field('description', line, True)
238 def get_authname(self):
239 return self._get_field('authname')
241 def set_authname(self, name):
242 self._set_field('authname', name or git.author().name)
244 def get_authemail(self):
245 return self._get_field('authemail')
247 def set_authemail(self, email):
248 self._set_field('authemail', email or git.author().email)
250 def get_authdate(self):
251 return self._get_field('authdate')
253 def set_authdate(self, date):
254 self._set_field('authdate', date or git.author().date)
256 def get_commname(self):
257 return self._get_field('commname')
259 def set_commname(self, name):
260 self._set_field('commname', name or git.committer().name)
262 def get_commemail(self):
263 return self._get_field('commemail')
265 def set_commemail(self, email):
266 self._set_field('commemail', email or git.committer().email)
268 def get_log(self):
269 return self._get_field('log')
271 def set_log(self, value, backup = False):
272 self._set_field('log', value)
273 self.__update_log_ref(value)
276 class Series(StgitObject):
277 """Class including the operations on series
279 def __init__(self, name = None):
280 """Takes a series name as the parameter.
282 try:
283 if name:
284 self.__name = name
285 else:
286 self.__name = git.get_head_file()
287 self.__base_dir = basedir.get()
288 except git.GitException, ex:
289 raise StackException, 'GIT tree not initialised: %s' % ex
291 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
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',
295 self.__name)
297 self.__applied_file = os.path.join(self._dir(), 'applied')
298 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
299 self.__hidden_file = os.path.join(self._dir(), 'hidden')
300 self.__current_file = os.path.join(self._dir(), 'current')
301 self.__descr_file = os.path.join(self._dir(), 'description')
303 # where this series keeps its patches
304 self.__patch_dir = os.path.join(self._dir(), 'patches')
305 if not os.path.isdir(self.__patch_dir):
306 self.__patch_dir = self._dir()
308 # if no __refs_dir, create and populate it (upgrade old repositories)
309 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
310 os.makedirs(self.__refs_dir)
311 for patch in self.get_applied() + self.get_unapplied():
312 self.get_patch(patch).update_top_ref()
314 # trash directory
315 self.__trash_dir = os.path.join(self._dir(), 'trash')
316 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
317 os.makedirs(self.__trash_dir)
319 def __patch_name_valid(self, name):
320 """Raise an exception if the patch name is not valid.
322 if not name or re.search('[^\w.-]', name):
323 raise StackException, 'Invalid patch name: "%s"' % name
325 def get_branch(self):
326 """Return the branch name for the Series object
328 return self.__name
330 def __set_current(self, name):
331 """Sets the topmost patch
333 self._set_field('current', name)
335 def get_patch(self, name):
336 """Return a Patch object for the given name
338 return Patch(name, self.__patch_dir, self.__refs_dir)
340 def get_current_patch(self):
341 """Return a Patch object representing the topmost patch, or
342 None if there is no such patch."""
343 crt = self.get_current()
344 if not crt:
345 return None
346 return Patch(crt, self.__patch_dir, self.__refs_dir)
348 def get_current(self):
349 """Return the name of the topmost patch, or None if there is
350 no such patch."""
351 name = self._get_field('current')
352 if name == '':
353 return None
354 else:
355 return name
357 def get_applied(self):
358 if not os.path.isfile(self.__applied_file):
359 raise StackException, 'Branch "%s" not initialised' % self.__name
360 f = file(self.__applied_file)
361 names = [line.strip() for line in f.readlines()]
362 f.close()
363 return names
365 def get_unapplied(self):
366 if not os.path.isfile(self.__unapplied_file):
367 raise StackException, 'Branch "%s" not initialised' % self.__name
368 f = file(self.__unapplied_file)
369 names = [line.strip() for line in f.readlines()]
370 f.close()
371 return names
373 def get_hidden(self):
374 if not os.path.isfile(self.__hidden_file):
375 return []
376 f = file(self.__hidden_file)
377 names = [line.strip() for line in f.readlines()]
378 f.close()
379 return names
381 def get_base_file(self):
382 self.__begin_stack_check()
383 return self.__base_file
385 def get_base(self):
386 return read_string(self.get_base_file())
388 def get_head(self):
389 """Return the head of the branch
391 crt = self.get_current_patch()
392 if crt:
393 return crt.get_top()
394 else:
395 return self.get_base()
397 def get_protected(self):
398 return os.path.isfile(os.path.join(self._dir(), 'protected'))
400 def protect(self):
401 protect_file = os.path.join(self._dir(), 'protected')
402 if not os.path.isfile(protect_file):
403 create_empty_file(protect_file)
405 def unprotect(self):
406 protect_file = os.path.join(self._dir(), 'protected')
407 if os.path.isfile(protect_file):
408 os.remove(protect_file)
410 def get_description(self):
411 return self._get_field('description') or ''
413 def set_description(self, line):
414 self._set_field('description', line)
416 def get_parent_remote(self):
417 value = config.get('branch.%s.remote' % self.__name)
418 if value:
419 return value
420 elif 'origin' in git.remotes_list():
421 print 'Notice: no parent remote declared for stack "%s", ' \
422 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
423 'and "branch.%s.merge" with "git repo-config".' \
424 % (self.__name, self.__name, self.__name)
425 return 'origin'
426 else:
427 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
429 def __set_parent_remote(self, remote):
430 value = config.set('branch.%s.remote' % self.__name, remote)
432 def get_parent_branch(self):
433 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
434 if value:
435 return value
436 elif git.rev_parse('heads/origin'):
437 print 'Notice: no parent branch declared for stack "%s", ' \
438 'defaulting to "heads/origin". Consider setting ' \
439 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
440 % (self.__name, self.__name)
441 return 'heads/origin'
442 else:
443 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
445 def __set_parent_branch(self, name):
446 if config.get('branch.%s.remote' % self.__name):
447 # Never set merge if remote is not set to avoid
448 # possibly-erroneous lookups into 'origin'
449 config.set('branch.%s.merge' % self.__name, name)
450 config.set('branch.%s.stgit.parentbranch' % self.__name, name)
452 def set_parent(self, remote, localbranch):
453 # policy: record local branches as remote='.'
454 recordremote = remote or '.'
455 if localbranch:
456 self.__set_parent_remote(recordremote)
457 self.__set_parent_branch(localbranch)
458 # We'll enforce this later
459 # else:
460 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
462 def __patch_is_current(self, patch):
463 return patch.get_name() == self.get_current()
465 def patch_applied(self, name):
466 """Return true if the patch exists in the applied list
468 return name in self.get_applied()
470 def patch_unapplied(self, name):
471 """Return true if the patch exists in the unapplied list
473 return name in self.get_unapplied()
475 def patch_hidden(self, name):
476 """Return true if the patch is hidden.
478 return name in self.get_hidden()
480 def patch_exists(self, name):
481 """Return true if there is a patch with the given name, false
482 otherwise."""
483 return self.patch_applied(name) or self.patch_unapplied(name)
485 def __begin_stack_check(self):
486 """Save the current HEAD into .git/refs/heads/base if the stack
487 is empty
489 if len(self.get_applied()) == 0:
490 head = git.get_head()
491 write_string(self.__base_file, head)
493 def __end_stack_check(self):
494 """Remove .git/refs/heads/base if the stack is empty.
495 This warning should never happen
497 if len(self.get_applied()) == 0 \
498 and read_string(self.__base_file) != git.get_head():
499 print 'Warning: stack empty but the HEAD and base are different'
501 def head_top_equal(self):
502 """Return true if the head and the top are the same
504 crt = self.get_current_patch()
505 if not crt:
506 # we don't care, no patches applied
507 return True
508 return git.get_head() == crt.get_top()
510 def is_initialised(self):
511 """Checks if series is already initialised
513 return os.path.isdir(self.__patch_dir)
515 def init(self, create_at=False, parent_remote=None, parent_branch=None):
516 """Initialises the stgit series
518 if os.path.exists(self.__patch_dir):
519 raise StackException, self.__patch_dir + ' already exists'
520 if os.path.exists(self.__refs_dir):
521 raise StackException, self.__refs_dir + ' already exists'
522 if os.path.exists(self.__base_file):
523 raise StackException, self.__base_file + ' already exists'
525 if (create_at!=False):
526 git.create_branch(self.__name, create_at)
528 os.makedirs(self.__patch_dir)
530 self.set_parent(parent_remote, parent_branch)
532 create_dirs(os.path.join(self.__base_dir, 'refs', 'bases'))
534 self.create_empty_field('applied')
535 self.create_empty_field('unapplied')
536 self.create_empty_field('description')
537 os.makedirs(os.path.join(self._dir(), 'patches'))
538 os.makedirs(self.__refs_dir)
539 self.__begin_stack_check()
540 self._set_field('orig-base', git.get_head())
542 def convert(self):
543 """Either convert to use a separate patch directory, or
544 unconvert to place the patches in the same directory with
545 series control files
547 if self.__patch_dir == self._dir():
548 print 'Converting old-style to new-style...',
549 sys.stdout.flush()
551 self.__patch_dir = os.path.join(self._dir(), 'patches')
552 os.makedirs(self.__patch_dir)
554 for p in self.get_applied() + self.get_unapplied():
555 src = os.path.join(self._dir(), p)
556 dest = os.path.join(self.__patch_dir, p)
557 os.rename(src, dest)
559 print 'done'
561 else:
562 print 'Converting new-style to old-style...',
563 sys.stdout.flush()
565 for p in self.get_applied() + self.get_unapplied():
566 src = os.path.join(self.__patch_dir, p)
567 dest = os.path.join(self._dir(), p)
568 os.rename(src, dest)
570 if not os.listdir(self.__patch_dir):
571 os.rmdir(self.__patch_dir)
572 print 'done'
573 else:
574 print 'Patch directory %s is not empty.' % self.__name
576 self.__patch_dir = self._dir()
578 def rename(self, to_name):
579 """Renames a series
581 to_stack = Series(to_name)
583 if to_stack.is_initialised():
584 raise StackException, '"%s" already exists' % to_stack.get_branch()
585 if os.path.exists(to_stack.__base_file):
586 os.remove(to_stack.__base_file)
588 git.rename_branch(self.__name, to_name)
590 if os.path.isdir(self._dir()):
591 rename(os.path.join(self.__base_dir, 'patches'),
592 self.__name, to_stack.__name)
593 if os.path.exists(self.__base_file):
594 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
595 self.__name, to_stack.__name)
596 if os.path.exists(self.__refs_dir):
597 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
598 self.__name, to_stack.__name)
600 # Rename the config section
601 config.rename_section("branch.%s" % self.__name,
602 "branch.%s" % to_name)
604 self.__init__(to_name)
606 def clone(self, target_series):
607 """Clones a series
609 try:
610 # allow cloning of branches not under StGIT control
611 base = self.get_base()
612 except:
613 base = git.get_head()
614 Series(target_series).init(create_at = base)
615 new_series = Series(target_series)
617 # generate an artificial description file
618 new_series.set_description('clone of "%s"' % self.__name)
620 # clone self's entire series as unapplied patches
621 try:
622 # allow cloning of branches not under StGIT control
623 applied = self.get_applied()
624 unapplied = self.get_unapplied()
625 patches = applied + unapplied
626 patches.reverse()
627 except:
628 patches = applied = unapplied = []
629 for p in patches:
630 patch = self.get_patch(p)
631 new_series.new_patch(p, message = patch.get_description(),
632 can_edit = False, unapplied = True,
633 bottom = patch.get_bottom(),
634 top = patch.get_top(),
635 author_name = patch.get_authname(),
636 author_email = patch.get_authemail(),
637 author_date = patch.get_authdate())
639 # fast forward the cloned series to self's top
640 new_series.forward_patches(applied)
642 # Clone remote and merge settings
643 value = config.get('branch.%s.remote' % self.__name)
644 if value:
645 config.set('branch.%s.remote' % target_series, value)
647 value = config.get('branch.%s.merge' % self.__name)
648 if value:
649 config.set('branch.%s.merge' % target_series, value)
651 def delete(self, force = False):
652 """Deletes an stgit series
654 if self.is_initialised():
655 patches = self.get_unapplied() + self.get_applied()
656 if not force and patches:
657 raise StackException, \
658 'Cannot delete: the series still contains patches'
659 for p in patches:
660 Patch(p, self.__patch_dir, self.__refs_dir).delete()
662 # remove the trash directory
663 for fname in os.listdir(self.__trash_dir):
664 os.remove(fname)
665 os.rmdir(self.__trash_dir)
667 # FIXME: find a way to get rid of those manual removals
668 # (move functionality to StgitObject ?)
669 if os.path.exists(self.__applied_file):
670 os.remove(self.__applied_file)
671 if os.path.exists(self.__unapplied_file):
672 os.remove(self.__unapplied_file)
673 if os.path.exists(self.__hidden_file):
674 os.remove(self.__hidden_file)
675 if os.path.exists(self.__current_file):
676 os.remove(self.__current_file)
677 if os.path.exists(self.__descr_file):
678 os.remove(self.__descr_file)
679 if not os.listdir(self.__patch_dir):
680 os.rmdir(self.__patch_dir)
681 else:
682 print 'Patch directory %s is not empty.' % self.__name
683 if not os.listdir(self._dir()):
684 remove_dirs(os.path.join(self.__base_dir, 'patches'),
685 self.__name)
686 else:
687 print 'Series directory %s is not empty.' % self.__name
688 if not os.listdir(self.__refs_dir):
689 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
690 self.__name)
691 else:
692 print 'Refs directory %s is not empty.' % self.__refs_dir
694 if os.path.exists(self.__base_file):
695 remove_file_and_dirs(
696 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
698 def refresh_patch(self, files = None, message = None, edit = False,
699 show_patch = False,
700 cache_update = True,
701 author_name = None, author_email = None,
702 author_date = None,
703 committer_name = None, committer_email = None,
704 backup = False, sign_str = None, log = 'refresh'):
705 """Generates a new commit for the given patch
707 name = self.get_current()
708 if not name:
709 raise StackException, 'No patches applied'
711 patch = Patch(name, self.__patch_dir, self.__refs_dir)
713 descr = patch.get_description()
714 if not (message or descr):
715 edit = True
716 descr = ''
717 elif message:
718 descr = message
720 if not message and edit:
721 descr = edit_file(self, descr.rstrip(), \
722 'Please edit the description for patch "%s" ' \
723 'above.' % name, show_patch)
725 if not author_name:
726 author_name = patch.get_authname()
727 if not author_email:
728 author_email = patch.get_authemail()
729 if not author_date:
730 author_date = patch.get_authdate()
731 if not committer_name:
732 committer_name = patch.get_commname()
733 if not committer_email:
734 committer_email = patch.get_commemail()
736 if sign_str:
737 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
738 committer_name, committer_email)
740 bottom = patch.get_bottom()
742 commit_id = git.commit(files = files,
743 message = descr, parents = [bottom],
744 cache_update = cache_update,
745 allowempty = True,
746 author_name = author_name,
747 author_email = author_email,
748 author_date = author_date,
749 committer_name = committer_name,
750 committer_email = committer_email)
752 patch.set_bottom(bottom, backup = backup)
753 patch.set_top(commit_id, backup = backup)
754 patch.set_description(descr)
755 patch.set_authname(author_name)
756 patch.set_authemail(author_email)
757 patch.set_authdate(author_date)
758 patch.set_commname(committer_name)
759 patch.set_commemail(committer_email)
761 if log:
762 self.log_patch(patch, log)
764 return commit_id
766 def undo_refresh(self):
767 """Undo the patch boundaries changes caused by 'refresh'
769 name = self.get_current()
770 assert(name)
772 patch = Patch(name, self.__patch_dir, self.__refs_dir)
773 old_bottom = patch.get_old_bottom()
774 old_top = patch.get_old_top()
776 # the bottom of the patch is not changed by refresh. If the
777 # old_bottom is different, there wasn't any previous 'refresh'
778 # command (probably only a 'push')
779 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
780 raise StackException, 'No undo information available'
782 git.reset(tree_id = old_top, check_out = False)
783 if patch.restore_old_boundaries():
784 self.log_patch(patch, 'undo')
786 def new_patch(self, name, message = None, can_edit = True,
787 unapplied = False, show_patch = False,
788 top = None, bottom = None,
789 author_name = None, author_email = None, author_date = None,
790 committer_name = None, committer_email = None,
791 before_existing = False, refresh = True):
792 """Creates a new patch
794 self.__patch_name_valid(name)
796 if self.patch_applied(name) or self.patch_unapplied(name):
797 raise StackException, 'Patch "%s" already exists' % name
799 if not message and can_edit:
800 descr = edit_file(self, None, \
801 'Please enter the description for patch "%s" ' \
802 'above.' % name, show_patch)
803 else:
804 descr = message
806 head = git.get_head()
808 self.__begin_stack_check()
810 patch = Patch(name, self.__patch_dir, self.__refs_dir)
811 patch.create()
813 if bottom:
814 patch.set_bottom(bottom)
815 else:
816 patch.set_bottom(head)
817 if top:
818 patch.set_top(top)
819 else:
820 patch.set_top(head)
822 patch.set_description(descr)
823 patch.set_authname(author_name)
824 patch.set_authemail(author_email)
825 patch.set_authdate(author_date)
826 patch.set_commname(committer_name)
827 patch.set_commemail(committer_email)
829 if unapplied:
830 self.log_patch(patch, 'new')
832 patches = [patch.get_name()] + self.get_unapplied()
834 f = file(self.__unapplied_file, 'w+')
835 f.writelines([line + '\n' for line in patches])
836 f.close()
837 elif before_existing:
838 self.log_patch(patch, 'new')
840 insert_string(self.__applied_file, patch.get_name())
841 if not self.get_current():
842 self.__set_current(name)
843 else:
844 append_string(self.__applied_file, patch.get_name())
845 self.__set_current(name)
846 if refresh:
847 self.refresh_patch(cache_update = False, log = 'new')
849 def delete_patch(self, name):
850 """Deletes a patch
852 self.__patch_name_valid(name)
853 patch = Patch(name, self.__patch_dir, self.__refs_dir)
855 if self.__patch_is_current(patch):
856 self.pop_patch(name)
857 elif self.patch_applied(name):
858 raise StackException, 'Cannot remove an applied patch, "%s", ' \
859 'which is not current' % name
860 elif not name in self.get_unapplied():
861 raise StackException, 'Unknown patch "%s"' % name
863 # save the commit id to a trash file
864 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
866 patch.delete()
868 unapplied = self.get_unapplied()
869 unapplied.remove(name)
870 f = file(self.__unapplied_file, 'w+')
871 f.writelines([line + '\n' for line in unapplied])
872 f.close()
874 if self.patch_hidden(name):
875 self.unhide_patch(name)
877 self.__begin_stack_check()
879 def forward_patches(self, names):
880 """Try to fast-forward an array of patches.
882 On return, patches in names[0:returned_value] have been pushed on the
883 stack. Apply the rest with push_patch
885 unapplied = self.get_unapplied()
886 self.__begin_stack_check()
888 forwarded = 0
889 top = git.get_head()
891 for name in names:
892 assert(name in unapplied)
894 patch = Patch(name, self.__patch_dir, self.__refs_dir)
896 head = top
897 bottom = patch.get_bottom()
898 top = patch.get_top()
900 # top != bottom always since we have a commit for each patch
901 if head == bottom:
902 # reset the backup information. No logging since the
903 # patch hasn't changed
904 patch.set_bottom(head, backup = True)
905 patch.set_top(top, backup = True)
907 else:
908 head_tree = git.get_commit(head).get_tree()
909 bottom_tree = git.get_commit(bottom).get_tree()
910 if head_tree == bottom_tree:
911 # We must just reparent this patch and create a new commit
912 # for it
913 descr = patch.get_description()
914 author_name = patch.get_authname()
915 author_email = patch.get_authemail()
916 author_date = patch.get_authdate()
917 committer_name = patch.get_commname()
918 committer_email = patch.get_commemail()
920 top_tree = git.get_commit(top).get_tree()
922 top = git.commit(message = descr, parents = [head],
923 cache_update = False,
924 tree_id = top_tree,
925 allowempty = True,
926 author_name = author_name,
927 author_email = author_email,
928 author_date = author_date,
929 committer_name = committer_name,
930 committer_email = committer_email)
932 patch.set_bottom(head, backup = True)
933 patch.set_top(top, backup = True)
935 self.log_patch(patch, 'push(f)')
936 else:
937 top = head
938 # stop the fast-forwarding, must do a real merge
939 break
941 forwarded+=1
942 unapplied.remove(name)
944 if forwarded == 0:
945 return 0
947 git.switch(top)
949 append_strings(self.__applied_file, names[0:forwarded])
951 f = file(self.__unapplied_file, 'w+')
952 f.writelines([line + '\n' for line in unapplied])
953 f.close()
955 self.__set_current(name)
957 return forwarded
959 def merged_patches(self, names):
960 """Test which patches were merged upstream by reverse-applying
961 them in reverse order. The function returns the list of
962 patches detected to have been applied. The state of the tree
963 is restored to the original one
965 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
966 for name in names]
967 patches.reverse()
969 merged = []
970 for p in patches:
971 if git.apply_diff(p.get_top(), p.get_bottom()):
972 merged.append(p.get_name())
973 merged.reverse()
975 git.reset()
977 return merged
979 def push_patch(self, name, empty = False):
980 """Pushes a patch on the stack
982 unapplied = self.get_unapplied()
983 assert(name in unapplied)
985 self.__begin_stack_check()
987 patch = Patch(name, self.__patch_dir, self.__refs_dir)
989 head = git.get_head()
990 bottom = patch.get_bottom()
991 top = patch.get_top()
993 ex = None
994 modified = False
996 # top != bottom always since we have a commit for each patch
997 if empty:
998 # just make an empty patch (top = bottom = HEAD). This
999 # option is useful to allow undoing already merged
1000 # patches. The top is updated by refresh_patch since we
1001 # need an empty commit
1002 patch.set_bottom(head, backup = True)
1003 patch.set_top(head, backup = True)
1004 modified = True
1005 elif head == bottom:
1006 # reset the backup information. No need for logging
1007 patch.set_bottom(bottom, backup = True)
1008 patch.set_top(top, backup = True)
1010 git.switch(top)
1011 else:
1012 # new patch needs to be refreshed.
1013 # The current patch is empty after merge.
1014 patch.set_bottom(head, backup = True)
1015 patch.set_top(head, backup = True)
1017 # Try the fast applying first. If this fails, fall back to the
1018 # three-way merge
1019 if not git.apply_diff(bottom, top):
1020 # if git.apply_diff() fails, the patch requires a diff3
1021 # merge and can be reported as modified
1022 modified = True
1024 # merge can fail but the patch needs to be pushed
1025 try:
1026 git.merge(bottom, head, top, recursive = True)
1027 except git.GitException, ex:
1028 print >> sys.stderr, \
1029 'The merge failed during "push". ' \
1030 'Use "refresh" after fixing the conflicts'
1032 append_string(self.__applied_file, name)
1034 unapplied.remove(name)
1035 f = file(self.__unapplied_file, 'w+')
1036 f.writelines([line + '\n' for line in unapplied])
1037 f.close()
1039 self.__set_current(name)
1041 # head == bottom case doesn't need to refresh the patch
1042 if empty or head != bottom:
1043 if not ex:
1044 # if the merge was OK and no conflicts, just refresh the patch
1045 # The GIT cache was already updated by the merge operation
1046 if modified:
1047 log = 'push(m)'
1048 else:
1049 log = 'push'
1050 self.refresh_patch(cache_update = False, log = log)
1051 else:
1052 # we store the correctly merged files only for
1053 # tracking the conflict history. Note that the
1054 # git.merge() operations should always leave the index
1055 # in a valid state (i.e. only stage 0 files)
1056 self.refresh_patch(cache_update = False, log = 'push(c)')
1057 raise StackException, str(ex)
1059 return modified
1061 def undo_push(self):
1062 name = self.get_current()
1063 assert(name)
1065 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1066 old_bottom = patch.get_old_bottom()
1067 old_top = patch.get_old_top()
1069 # the top of the patch is changed by a push operation only
1070 # together with the bottom (otherwise the top was probably
1071 # modified by 'refresh'). If they are both unchanged, there
1072 # was a fast forward
1073 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1074 raise StackException, 'No undo information available'
1076 git.reset()
1077 self.pop_patch(name)
1078 ret = patch.restore_old_boundaries()
1079 if ret:
1080 self.log_patch(patch, 'undo')
1082 return ret
1084 def pop_patch(self, name, keep = False):
1085 """Pops the top patch from the stack
1087 applied = self.get_applied()
1088 applied.reverse()
1089 assert(name in applied)
1091 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1093 # only keep the local changes
1094 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1095 raise StackException, \
1096 'Failed to pop patches while preserving the local changes'
1098 git.switch(patch.get_bottom(), keep)
1100 # save the new applied list
1101 idx = applied.index(name) + 1
1103 popped = applied[:idx]
1104 popped.reverse()
1105 unapplied = popped + self.get_unapplied()
1107 f = file(self.__unapplied_file, 'w+')
1108 f.writelines([line + '\n' for line in unapplied])
1109 f.close()
1111 del applied[:idx]
1112 applied.reverse()
1114 f = file(self.__applied_file, 'w+')
1115 f.writelines([line + '\n' for line in applied])
1116 f.close()
1118 if applied == []:
1119 self.__set_current(None)
1120 else:
1121 self.__set_current(applied[-1])
1123 self.__end_stack_check()
1125 def empty_patch(self, name):
1126 """Returns True if the patch is empty
1128 self.__patch_name_valid(name)
1129 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1130 bottom = patch.get_bottom()
1131 top = patch.get_top()
1133 if bottom == top:
1134 return True
1135 elif git.get_commit(top).get_tree() \
1136 == git.get_commit(bottom).get_tree():
1137 return True
1139 return False
1141 def rename_patch(self, oldname, newname):
1142 self.__patch_name_valid(newname)
1144 applied = self.get_applied()
1145 unapplied = self.get_unapplied()
1147 if oldname == newname:
1148 raise StackException, '"To" name and "from" name are the same'
1150 if newname in applied or newname in unapplied:
1151 raise StackException, 'Patch "%s" already exists' % newname
1153 if self.patch_hidden(oldname):
1154 self.unhide_patch(oldname)
1155 self.hide_patch(newname)
1157 if oldname in unapplied:
1158 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1159 unapplied[unapplied.index(oldname)] = newname
1161 f = file(self.__unapplied_file, 'w+')
1162 f.writelines([line + '\n' for line in unapplied])
1163 f.close()
1164 elif oldname in applied:
1165 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1166 if oldname == self.get_current():
1167 self.__set_current(newname)
1169 applied[applied.index(oldname)] = newname
1171 f = file(self.__applied_file, 'w+')
1172 f.writelines([line + '\n' for line in applied])
1173 f.close()
1174 else:
1175 raise StackException, 'Unknown patch "%s"' % oldname
1177 def log_patch(self, patch, message):
1178 """Generate a log commit for a patch
1180 top = git.get_commit(patch.get_top())
1181 msg = '%s\t%s' % (message, top.get_id_hash())
1183 old_log = patch.get_log()
1184 if old_log:
1185 parents = [old_log]
1186 else:
1187 parents = []
1189 log = git.commit(message = msg, parents = parents,
1190 cache_update = False, tree_id = top.get_tree(),
1191 allowempty = True)
1192 patch.set_log(log)
1194 def hide_patch(self, name):
1195 """Add the patch to the hidden list.
1197 if not self.patch_exists(name):
1198 raise StackException, 'Unknown patch "%s"' % name
1199 elif self.patch_hidden(name):
1200 raise StackException, 'Patch "%s" already hidden' % name
1202 append_string(self.__hidden_file, name)
1204 def unhide_patch(self, name):
1205 """Add the patch to the hidden list.
1207 if not self.patch_exists(name):
1208 raise StackException, 'Unknown patch "%s"' % name
1209 hidden = self.get_hidden()
1210 if not name in hidden:
1211 raise StackException, 'Patch "%s" not hidden' % name
1213 hidden.remove(name)
1215 f = file(self.__hidden_file, 'w+')
1216 f.writelines([line + '\n' for line in hidden])
1217 f.close()