1 """Basic quilt-like functionality
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
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):
34 self.should_print = True
35 def __call__(self, x, until_test, prefix):
37 self.should_print = False
39 return x[0:len(prefix)] != prefix
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
48 def __clean_comments(f):
49 """Removes lines marked for status in a commit file
53 # remove status-prefixed lines
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':
64 f.seek(0); f.truncate()
67 def edit_file(series, line, comment, show_patch = True):
68 fname = '.stgitmsg.txt'
69 tmpl = templates.get_template('patchdescr.tmpl')
78 print >> f, __comment_prefix, comment
79 print >> f, __comment_prefix, \
80 'Lines prefixed with "%s" will be automatically removed.' \
82 print >> f, __comment_prefix, \
83 'Trailing empty lines will be automatically removed.'
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:'
95 if config.has_option('stgit', 'editor'):
96 editor = config.get('stgit', 'editor')
97 elif 'EDITOR' in os.environ:
98 editor = os.environ['EDITOR']
101 editor += ' %s' % fname
103 print 'Invoking the editor: "%s"...' % editor,
105 print 'done (exit code: %d)' % os.system(editor)
107 f = file(fname, 'r+')
123 """An object with stgit-like properties stored as files in a directory
125 def _set_dir(self, dir):
130 def create_empty_field(self, name):
131 create_empty_file(os.path.join(self.__dir, name))
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)
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):
152 class Patch(StgitObject):
153 """Basic patch implementation
155 def __init__(self, name, series_dir, refs_dir):
156 self.__series_dir = series_dir
158 self._set_dir(os.path.join(self.__series_dir, self.__name))
159 self.__refs_dir = refs_dir
160 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
161 self.__log_ref_file = os.path.join(self.__refs_dir,
162 self.__name + '.log')
165 os.mkdir(self._dir())
166 self.create_empty_field('bottom')
167 self.create_empty_field('top')
170 for f in os.listdir(self._dir()):
171 os.remove(os.path.join(self._dir(), f))
172 os.rmdir(self._dir())
173 os.remove(self.__top_ref_file)
174 if os.path.exists(self.__log_ref_file):
175 os.remove(self.__log_ref_file)
180 def rename(self, newname):
182 old_top_ref_file = self.__top_ref_file
183 old_log_ref_file = self.__log_ref_file
184 self.__name = newname
185 self._set_dir(os.path.join(self.__series_dir, self.__name))
186 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
187 self.__log_ref_file = os.path.join(self.__refs_dir,
188 self.__name + '.log')
190 os.rename(olddir, self._dir())
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)
195 def __update_top_ref(self, ref):
196 write_string(self.__top_ref_file, ref)
198 def __update_log_ref(self, ref):
199 write_string(self.__log_ref_file, ref)
201 def update_top_ref(self):
204 self.__update_top_ref(top)
206 def get_old_bottom(self):
207 return self._get_field('bottom.old')
209 def get_bottom(self):
210 return self._get_field('bottom')
212 def set_bottom(self, value, backup = False):
214 curr = self._get_field('bottom')
215 self._set_field('bottom.old', curr)
216 self._set_field('bottom', value)
218 def get_old_top(self):
219 return self._get_field('top.old')
222 return self._get_field('top')
224 def set_top(self, value, backup = False):
226 curr = self._get_field('top')
227 self._set_field('top.old', curr)
228 self._set_field('top', value)
229 self.__update_top_ref(value)
231 def restore_old_boundaries(self):
232 bottom = self._get_field('bottom.old')
233 top = self._get_field('top.old')
236 self._set_field('bottom', bottom)
237 self._set_field('top', top)
238 self.__update_top_ref(top)
243 def get_description(self):
244 return self._get_field('description', True)
246 def set_description(self, line):
247 self._set_field('description', line, True)
249 def get_authname(self):
250 return self._get_field('authname')
252 def set_authname(self, name):
253 self._set_field('authname', name or git.author().name)
255 def get_authemail(self):
256 return self._get_field('authemail')
258 def set_authemail(self, email):
259 self._set_field('authemail', email or git.author().email)
261 def get_authdate(self):
262 return self._get_field('authdate')
264 def set_authdate(self, date):
265 self._set_field('authdate', date or git.author().date)
267 def get_commname(self):
268 return self._get_field('commname')
270 def set_commname(self, name):
271 self._set_field('commname', name or git.committer().name)
273 def get_commemail(self):
274 return self._get_field('commemail')
276 def set_commemail(self, email):
277 self._set_field('commemail', email or git.committer().email)
280 return self._get_field('log')
282 def set_log(self, value, backup = False):
283 self._set_field('log', value)
284 self.__update_log_ref(value)
287 class Series(StgitObject):
288 """Class including the operations on series
290 def __init__(self, name = None):
291 """Takes a series name as the parameter.
297 self.__name = git.get_head_file()
298 self.__base_dir = basedir.get()
299 except git.GitException, ex:
300 raise StackException, 'GIT tree not initialised: %s' % ex
302 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
303 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
305 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
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')
313 # where this series keeps its patches
314 self.__patch_dir = os.path.join(self._dir(), 'patches')
315 if not os.path.isdir(self.__patch_dir):
316 self.__patch_dir = self._dir()
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()
325 self.__trash_dir = os.path.join(self._dir(), 'trash')
326 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
327 os.makedirs(self.__trash_dir)
329 def get_branch(self):
330 """Return the branch name for the Series object
334 def __set_current(self, name):
335 """Sets the topmost patch
337 self._set_field('current', name)
339 def get_patch(self, name):
340 """Return a Patch object for the given name
342 return Patch(name, self.__patch_dir, self.__refs_dir)
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()
350 return Patch(crt, self.__patch_dir, self.__refs_dir)
352 def get_current(self):
353 """Return the name of the topmost patch, or None if there is
355 name = self._get_field('current')
361 def get_applied(self):
362 if not os.path.isfile(self.__applied_file):
363 raise StackException, 'Branch "%s" not initialised' % self.__name
364 f = file(self.__applied_file)
365 names = [line.strip() for line in f.readlines()]
369 def get_unapplied(self):
370 if not os.path.isfile(self.__unapplied_file):
371 raise StackException, 'Branch "%s" not initialised' % self.__name
372 f = file(self.__unapplied_file)
373 names = [line.strip() for line in f.readlines()]
377 def get_base_file(self):
378 self.__begin_stack_check()
379 return self.__base_file
381 def get_protected(self):
382 return os.path.isfile(os.path.join(self._dir(), 'protected'))
385 protect_file = os.path.join(self._dir(), 'protected')
386 if not os.path.isfile(protect_file):
387 create_empty_file(protect_file)
390 protect_file = os.path.join(self._dir(), 'protected')
391 if os.path.isfile(protect_file):
392 os.remove(protect_file)
394 def get_description(self):
395 return self._get_field('description') or ''
397 def set_description(self, line):
398 self._set_field('description', line)
400 def __patch_is_current(self, patch):
401 return patch.get_name() == self.get_current()
403 def patch_applied(self, name):
404 """Return true if the patch exists in the applied list
406 return name in self.get_applied()
408 def patch_unapplied(self, name):
409 """Return true if the patch exists in the unapplied list
411 return name in self.get_unapplied()
413 def patch_exists(self, name):
414 """Return true if there is a patch with the given name, false
416 return self.patch_applied(name) or self.patch_unapplied(name)
418 def __begin_stack_check(self):
419 """Save the current HEAD into .git/refs/heads/base if the stack
422 if len(self.get_applied()) == 0:
423 head = git.get_head()
424 write_string(self.__base_file, head)
426 def __end_stack_check(self):
427 """Remove .git/refs/heads/base if the stack is empty.
428 This warning should never happen
430 if len(self.get_applied()) == 0 \
431 and read_string(self.__base_file) != git.get_head():
432 print 'Warning: stack empty but the HEAD and base are different'
434 def head_top_equal(self):
435 """Return true if the head and the top are the same
437 crt = self.get_current_patch()
439 # we don't care, no patches applied
441 return git.get_head() == crt.get_top()
443 def is_initialised(self):
444 """Checks if series is already initialised
446 return os.path.isdir(self.__patch_dir)
448 def init(self, create_at=False):
449 """Initialises the stgit series
451 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
453 if os.path.exists(self.__patch_dir):
454 raise StackException, self.__patch_dir + ' already exists'
455 if os.path.exists(self.__refs_dir):
456 raise StackException, self.__refs_dir + ' already exists'
457 if os.path.exists(self.__base_file):
458 raise StackException, self.__base_file + ' already exists'
460 if (create_at!=False):
461 git.create_branch(self.__name, create_at)
463 os.makedirs(self.__patch_dir)
465 create_dirs(bases_dir)
467 self.create_empty_field('applied')
468 self.create_empty_field('unapplied')
469 self.create_empty_field('description')
470 os.makedirs(os.path.join(self._dir(), 'patches'))
471 os.makedirs(self.__refs_dir)
472 self.__begin_stack_check()
475 """Either convert to use a separate patch directory, or
476 unconvert to place the patches in the same directory with
479 if self.__patch_dir == self._dir():
480 print 'Converting old-style to new-style...',
483 self.__patch_dir = os.path.join(self._dir(), 'patches')
484 os.makedirs(self.__patch_dir)
486 for p in self.get_applied() + self.get_unapplied():
487 src = os.path.join(self._dir(), p)
488 dest = os.path.join(self.__patch_dir, p)
494 print 'Converting new-style to old-style...',
497 for p in self.get_applied() + self.get_unapplied():
498 src = os.path.join(self.__patch_dir, p)
499 dest = os.path.join(self._dir(), p)
502 if not os.listdir(self.__patch_dir):
503 os.rmdir(self.__patch_dir)
506 print 'Patch directory %s is not empty.' % self.__name
508 self.__patch_dir = self._dir()
510 def rename(self, to_name):
513 to_stack = Series(to_name)
515 if to_stack.is_initialised():
516 raise StackException, '"%s" already exists' % to_stack.get_branch()
517 if os.path.exists(to_stack.__base_file):
518 os.remove(to_stack.__base_file)
520 git.rename_branch(self.__name, to_name)
522 if os.path.isdir(self._dir()):
523 rename(os.path.join(self.__base_dir, 'patches'),
524 self.__name, to_stack.__name)
525 if os.path.exists(self.__base_file):
526 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
527 self.__name, to_stack.__name)
528 if os.path.exists(self.__refs_dir):
529 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
530 self.__name, to_stack.__name)
532 self.__init__(to_name)
534 def clone(self, target_series):
538 # allow cloning of branches not under StGIT control
539 base = read_string(self.get_base_file())
541 base = git.get_head()
542 Series(target_series).init(create_at = base)
543 new_series = Series(target_series)
545 # generate an artificial description file
546 new_series.set_description('clone of "%s"' % self.__name)
548 # clone self's entire series as unapplied patches
550 # allow cloning of branches not under StGIT control
551 applied = self.get_applied()
552 unapplied = self.get_unapplied()
553 patches = applied + unapplied
556 patches = applied = unapplied = []
558 patch = self.get_patch(p)
559 new_series.new_patch(p, message = patch.get_description(),
560 can_edit = False, unapplied = True,
561 bottom = patch.get_bottom(),
562 top = patch.get_top(),
563 author_name = patch.get_authname(),
564 author_email = patch.get_authemail(),
565 author_date = patch.get_authdate())
567 # fast forward the cloned series to self's top
568 new_series.forward_patches(applied)
570 def delete(self, force = False):
571 """Deletes an stgit series
573 if self.is_initialised():
574 patches = self.get_unapplied() + self.get_applied()
575 if not force and patches:
576 raise StackException, \
577 'Cannot delete: the series still contains patches'
579 Patch(p, self.__patch_dir, self.__refs_dir).delete()
581 # remove the trash directory
582 for fname in os.listdir(self.__trash_dir):
584 os.rmdir(self.__trash_dir)
586 # FIXME: find a way to get rid of those manual removals
587 # (move functionnality to StgitObject ?)
588 if os.path.exists(self.__applied_file):
589 os.remove(self.__applied_file)
590 if os.path.exists(self.__unapplied_file):
591 os.remove(self.__unapplied_file)
592 if os.path.exists(self.__current_file):
593 os.remove(self.__current_file)
594 if os.path.exists(self.__descr_file):
595 os.remove(self.__descr_file)
596 if not os.listdir(self.__patch_dir):
597 os.rmdir(self.__patch_dir)
599 print 'Patch directory %s is not empty.' % self.__name
600 if not os.listdir(self._dir()):
601 remove_dirs(os.path.join(self.__base_dir, 'patches'),
604 print 'Series directory %s is not empty.' % self.__name
605 if not os.listdir(self.__refs_dir):
606 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
609 print 'Refs directory %s is not empty.' % self.__refs_dir
611 if os.path.exists(self.__base_file):
612 remove_file_and_dirs(
613 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
615 def refresh_patch(self, files = None, message = None, edit = False,
618 author_name = None, author_email = None,
620 committer_name = None, committer_email = None,
621 backup = False, sign_str = None, log = 'refresh'):
622 """Generates a new commit for the given patch
624 name = self.get_current()
626 raise StackException, 'No patches applied'
628 patch = Patch(name, self.__patch_dir, self.__refs_dir)
630 descr = patch.get_description()
631 if not (message or descr):
637 if not message and edit:
638 descr = edit_file(self, descr.rstrip(), \
639 'Please edit the description for patch "%s" ' \
640 'above.' % name, show_patch)
643 author_name = patch.get_authname()
645 author_email = patch.get_authemail()
647 author_date = patch.get_authdate()
648 if not committer_name:
649 committer_name = patch.get_commname()
650 if not committer_email:
651 committer_email = patch.get_commemail()
654 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
655 committer_name, committer_email)
657 bottom = patch.get_bottom()
659 commit_id = git.commit(files = files,
660 message = descr, parents = [bottom],
661 cache_update = cache_update,
663 author_name = author_name,
664 author_email = author_email,
665 author_date = author_date,
666 committer_name = committer_name,
667 committer_email = committer_email)
669 patch.set_bottom(bottom, backup = backup)
670 patch.set_top(commit_id, backup = backup)
671 patch.set_description(descr)
672 patch.set_authname(author_name)
673 patch.set_authemail(author_email)
674 patch.set_authdate(author_date)
675 patch.set_commname(committer_name)
676 patch.set_commemail(committer_email)
679 self.log_patch(patch, log)
683 def undo_refresh(self):
684 """Undo the patch boundaries changes caused by 'refresh'
686 name = self.get_current()
689 patch = Patch(name, self.__patch_dir, self.__refs_dir)
690 old_bottom = patch.get_old_bottom()
691 old_top = patch.get_old_top()
693 # the bottom of the patch is not changed by refresh. If the
694 # old_bottom is different, there wasn't any previous 'refresh'
695 # command (probably only a 'push')
696 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
697 raise StackException, 'No undo information available'
699 git.reset(tree_id = old_top, check_out = False)
700 if patch.restore_old_boundaries():
701 self.log_patch(patch, 'undo')
703 def new_patch(self, name, message = None, can_edit = True,
704 unapplied = False, show_patch = False,
705 top = None, bottom = None,
706 author_name = None, author_email = None, author_date = None,
707 committer_name = None, committer_email = None,
708 before_existing = False, refresh = True):
709 """Creates a new patch
711 if self.patch_applied(name) or self.patch_unapplied(name):
712 raise StackException, 'Patch "%s" already exists' % name
714 if not message and can_edit:
715 descr = edit_file(self, None, \
716 'Please enter the description for patch "%s" ' \
717 'above.' % name, show_patch)
721 head = git.get_head()
723 self.__begin_stack_check()
725 patch = Patch(name, self.__patch_dir, self.__refs_dir)
729 patch.set_bottom(bottom)
731 patch.set_bottom(head)
737 patch.set_description(descr)
738 patch.set_authname(author_name)
739 patch.set_authemail(author_email)
740 patch.set_authdate(author_date)
741 patch.set_commname(committer_name)
742 patch.set_commemail(committer_email)
745 self.log_patch(patch, 'new')
747 patches = [patch.get_name()] + self.get_unapplied()
749 f = file(self.__unapplied_file, 'w+')
750 f.writelines([line + '\n' for line in patches])
752 elif before_existing:
753 self.log_patch(patch, 'new')
755 insert_string(self.__applied_file, patch.get_name())
756 if not self.get_current():
757 self.__set_current(name)
759 append_string(self.__applied_file, patch.get_name())
760 self.__set_current(name)
762 self.refresh_patch(cache_update = False, log = 'new')
764 def delete_patch(self, name):
767 patch = Patch(name, self.__patch_dir, self.__refs_dir)
769 if self.__patch_is_current(patch):
771 elif self.patch_applied(name):
772 raise StackException, 'Cannot remove an applied patch, "%s", ' \
773 'which is not current' % name
774 elif not name in self.get_unapplied():
775 raise StackException, 'Unknown patch "%s"' % name
777 # save the commit id to a trash file
778 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
782 unapplied = self.get_unapplied()
783 unapplied.remove(name)
784 f = file(self.__unapplied_file, 'w+')
785 f.writelines([line + '\n' for line in unapplied])
787 self.__begin_stack_check()
789 def forward_patches(self, names):
790 """Try to fast-forward an array of patches.
792 On return, patches in names[0:returned_value] have been pushed on the
793 stack. Apply the rest with push_patch
795 unapplied = self.get_unapplied()
796 self.__begin_stack_check()
802 assert(name in unapplied)
804 patch = Patch(name, self.__patch_dir, self.__refs_dir)
807 bottom = patch.get_bottom()
808 top = patch.get_top()
810 # top != bottom always since we have a commit for each patch
812 # reset the backup information. No logging since the
813 # patch hasn't changed
814 patch.set_bottom(head, backup = True)
815 patch.set_top(top, backup = True)
818 head_tree = git.get_commit(head).get_tree()
819 bottom_tree = git.get_commit(bottom).get_tree()
820 if head_tree == bottom_tree:
821 # We must just reparent this patch and create a new commit
823 descr = patch.get_description()
824 author_name = patch.get_authname()
825 author_email = patch.get_authemail()
826 author_date = patch.get_authdate()
827 committer_name = patch.get_commname()
828 committer_email = patch.get_commemail()
830 top_tree = git.get_commit(top).get_tree()
832 top = git.commit(message = descr, parents = [head],
833 cache_update = False,
836 author_name = author_name,
837 author_email = author_email,
838 author_date = author_date,
839 committer_name = committer_name,
840 committer_email = committer_email)
842 patch.set_bottom(head, backup = True)
843 patch.set_top(top, backup = True)
845 self.log_patch(patch, 'push(f)')
848 # stop the fast-forwarding, must do a real merge
852 unapplied.remove(name)
859 append_strings(self.__applied_file, names[0:forwarded])
861 f = file(self.__unapplied_file, 'w+')
862 f.writelines([line + '\n' for line in unapplied])
865 self.__set_current(name)
869 def merged_patches(self, names):
870 """Test which patches were merged upstream by reverse-applying
871 them in reverse order. The function returns the list of
872 patches detected to have been applied. The state of the tree
873 is restored to the original one
875 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
881 if git.apply_diff(p.get_top(), p.get_bottom()):
882 merged.append(p.get_name())
889 def push_patch(self, name, empty = False):
890 """Pushes a patch on the stack
892 unapplied = self.get_unapplied()
893 assert(name in unapplied)
895 self.__begin_stack_check()
897 patch = Patch(name, self.__patch_dir, self.__refs_dir)
899 head = git.get_head()
900 bottom = patch.get_bottom()
901 top = patch.get_top()
906 # top != bottom always since we have a commit for each patch
908 # just make an empty patch (top = bottom = HEAD). This
909 # option is useful to allow undoing already merged
910 # patches. The top is updated by refresh_patch since we
911 # need an empty commit
912 patch.set_bottom(head, backup = True)
913 patch.set_top(head, backup = True)
916 # reset the backup information. No need for logging
917 patch.set_bottom(bottom, backup = True)
918 patch.set_top(top, backup = True)
922 # new patch needs to be refreshed.
923 # The current patch is empty after merge.
924 patch.set_bottom(head, backup = True)
925 patch.set_top(head, backup = True)
927 # Try the fast applying first. If this fails, fall back to the
929 if not git.apply_diff(bottom, top):
930 # if git.apply_diff() fails, the patch requires a diff3
931 # merge and can be reported as modified
934 # merge can fail but the patch needs to be pushed
936 git.merge(bottom, head, top)
937 except git.GitException, ex:
938 print >> sys.stderr, \
939 'The merge failed during "push". ' \
940 'Use "refresh" after fixing the conflicts'
942 append_string(self.__applied_file, name)
944 unapplied.remove(name)
945 f = file(self.__unapplied_file, 'w+')
946 f.writelines([line + '\n' for line in unapplied])
949 self.__set_current(name)
951 # head == bottom case doesn't need to refresh the patch
952 if empty or head != bottom:
954 # if the merge was OK and no conflicts, just refresh the patch
955 # The GIT cache was already updated by the merge operation
960 self.refresh_patch(cache_update = False, log = log)
962 raise StackException, str(ex)
967 name = self.get_current()
970 patch = Patch(name, self.__patch_dir, self.__refs_dir)
971 old_bottom = patch.get_old_bottom()
972 old_top = patch.get_old_top()
974 # the top of the patch is changed by a push operation only
975 # together with the bottom (otherwise the top was probably
976 # modified by 'refresh'). If they are both unchanged, there
978 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
979 raise StackException, 'No undo information available'
983 ret = patch.restore_old_boundaries()
985 self.log_patch(patch, 'undo')
989 def pop_patch(self, name, keep = False):
990 """Pops the top patch from the stack
992 applied = self.get_applied()
994 assert(name in applied)
996 patch = Patch(name, self.__patch_dir, self.__refs_dir)
998 # only keep the local changes
999 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1000 raise StackException, \
1001 'Failed to pop patches while preserving the local changes'
1003 git.switch(patch.get_bottom(), keep)
1005 # save the new applied list
1006 idx = applied.index(name) + 1
1008 popped = applied[:idx]
1010 unapplied = popped + self.get_unapplied()
1012 f = file(self.__unapplied_file, 'w+')
1013 f.writelines([line + '\n' for line in unapplied])
1019 f = file(self.__applied_file, 'w+')
1020 f.writelines([line + '\n' for line in applied])
1024 self.__set_current(None)
1026 self.__set_current(applied[-1])
1028 self.__end_stack_check()
1030 def empty_patch(self, name):
1031 """Returns True if the patch is empty
1033 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1034 bottom = patch.get_bottom()
1035 top = patch.get_top()
1039 elif git.get_commit(top).get_tree() \
1040 == git.get_commit(bottom).get_tree():
1045 def rename_patch(self, oldname, newname):
1046 applied = self.get_applied()
1047 unapplied = self.get_unapplied()
1049 if oldname == newname:
1050 raise StackException, '"To" name and "from" name are the same'
1052 if newname in applied or newname in unapplied:
1053 raise StackException, 'Patch "%s" already exists' % newname
1055 if oldname in unapplied:
1056 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1057 unapplied[unapplied.index(oldname)] = newname
1059 f = file(self.__unapplied_file, 'w+')
1060 f.writelines([line + '\n' for line in unapplied])
1062 elif oldname in applied:
1063 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1064 if oldname == self.get_current():
1065 self.__set_current(newname)
1067 applied[applied.index(oldname)] = newname
1069 f = file(self.__applied_file, 'w+')
1070 f.writelines([line + '\n' for line in applied])
1073 raise StackException, 'Unknown patch "%s"' % oldname
1075 def log_patch(self, patch, message):
1076 """Generate a log commit for a patch
1078 top = git.get_commit(patch.get_top())
1079 msg = '%s\t%s' % (message, top.get_id_hash())
1081 old_log = patch.get_log()
1087 log = git.commit(message = msg, parents = parents,
1088 cache_update = False, tree_id = top.get_tree(),