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 editor = config.get('stgit.editor')
98 elif 'EDITOR' in os.environ:
99 editor = os.environ['EDITOR']
102 editor += ' %s' % fname
104 print 'Invoking the editor: "%s"...' % editor,
106 print 'done (exit code: %d)' % os.system(editor)
108 f = file(fname, 'r+')
124 """An object with stgit-like properties stored as files in a directory
126 def _set_dir(self, dir):
131 def create_empty_field(self, name):
132 create_empty_file(os.path.join(self.__dir, name))
134 def _get_field(self, name, multiline = False):
135 id_file = os.path.join(self.__dir, name)
136 if os.path.isfile(id_file):
137 line = read_string(id_file, multiline)
145 def _set_field(self, name, value, multiline = False):
146 fname = os.path.join(self.__dir, name)
147 if value and value != '':
148 write_string(fname, value, multiline)
149 elif os.path.isfile(fname):
153 class Patch(StgitObject):
154 """Basic patch implementation
156 def __init__(self, name, series_dir, refs_dir):
157 self.__series_dir = series_dir
159 self._set_dir(os.path.join(self.__series_dir, self.__name))
160 self.__refs_dir = refs_dir
161 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
162 self.__log_ref_file = os.path.join(self.__refs_dir,
163 self.__name + '.log')
166 os.mkdir(self._dir())
167 self.create_empty_field('bottom')
168 self.create_empty_field('top')
171 for f in os.listdir(self._dir()):
172 os.remove(os.path.join(self._dir(), f))
173 os.rmdir(self._dir())
174 os.remove(self.__top_ref_file)
175 if os.path.exists(self.__log_ref_file):
176 os.remove(self.__log_ref_file)
181 def rename(self, newname):
183 old_top_ref_file = self.__top_ref_file
184 old_log_ref_file = self.__log_ref_file
185 self.__name = newname
186 self._set_dir(os.path.join(self.__series_dir, self.__name))
187 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
188 self.__log_ref_file = os.path.join(self.__refs_dir,
189 self.__name + '.log')
191 os.rename(olddir, self._dir())
192 os.rename(old_top_ref_file, self.__top_ref_file)
193 if os.path.exists(old_log_ref_file):
194 os.rename(old_log_ref_file, self.__log_ref_file)
196 def __update_top_ref(self, ref):
197 write_string(self.__top_ref_file, ref)
199 def __update_log_ref(self, ref):
200 write_string(self.__log_ref_file, ref)
202 def update_top_ref(self):
205 self.__update_top_ref(top)
207 def get_old_bottom(self):
208 return self._get_field('bottom.old')
210 def get_bottom(self):
211 return self._get_field('bottom')
213 def set_bottom(self, value, backup = False):
215 curr = self._get_field('bottom')
216 self._set_field('bottom.old', curr)
217 self._set_field('bottom', value)
219 def get_old_top(self):
220 return self._get_field('top.old')
223 return self._get_field('top')
225 def set_top(self, value, backup = False):
227 curr = self._get_field('top')
228 self._set_field('top.old', curr)
229 self._set_field('top', value)
230 self.__update_top_ref(value)
232 def restore_old_boundaries(self):
233 bottom = self._get_field('bottom.old')
234 top = self._get_field('top.old')
237 self._set_field('bottom', bottom)
238 self._set_field('top', top)
239 self.__update_top_ref(top)
244 def get_description(self):
245 return self._get_field('description', True)
247 def set_description(self, line):
248 self._set_field('description', line, True)
250 def get_authname(self):
251 return self._get_field('authname')
253 def set_authname(self, name):
254 self._set_field('authname', name or git.author().name)
256 def get_authemail(self):
257 return self._get_field('authemail')
259 def set_authemail(self, email):
260 self._set_field('authemail', email or git.author().email)
262 def get_authdate(self):
263 return self._get_field('authdate')
265 def set_authdate(self, date):
266 self._set_field('authdate', date or git.author().date)
268 def get_commname(self):
269 return self._get_field('commname')
271 def set_commname(self, name):
272 self._set_field('commname', name or git.committer().name)
274 def get_commemail(self):
275 return self._get_field('commemail')
277 def set_commemail(self, email):
278 self._set_field('commemail', email or git.committer().email)
281 return self._get_field('log')
283 def set_log(self, value, backup = False):
284 self._set_field('log', value)
285 self.__update_log_ref(value)
288 class Series(StgitObject):
289 """Class including the operations on series
291 def __init__(self, name = None):
292 """Takes a series name as the parameter.
298 self.__name = git.get_head_file()
299 self.__base_dir = basedir.get()
300 except git.GitException, ex:
301 raise StackException, 'GIT tree not initialised: %s' % ex
303 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
304 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
306 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
309 self.__applied_file = os.path.join(self._dir(), 'applied')
310 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
311 self.__hidden_file = os.path.join(self._dir(), 'hidden')
312 self.__current_file = os.path.join(self._dir(), 'current')
313 self.__descr_file = os.path.join(self._dir(), 'description')
315 # where this series keeps its patches
316 self.__patch_dir = os.path.join(self._dir(), 'patches')
317 if not os.path.isdir(self.__patch_dir):
318 self.__patch_dir = self._dir()
320 # if no __refs_dir, create and populate it (upgrade old repositories)
321 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
322 os.makedirs(self.__refs_dir)
323 for patch in self.get_applied() + self.get_unapplied():
324 self.get_patch(patch).update_top_ref()
327 self.__trash_dir = os.path.join(self._dir(), 'trash')
328 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
329 os.makedirs(self.__trash_dir)
331 def __patch_name_valid(self, name):
332 """Raise an exception if the patch name is not valid.
334 if not name or re.search('[^\w.-]', name):
335 raise StackException, 'Invalid patch name: "%s"' % name
337 def get_branch(self):
338 """Return the branch name for the Series object
342 def __set_current(self, name):
343 """Sets the topmost patch
345 self._set_field('current', name)
347 def get_patch(self, name):
348 """Return a Patch object for the given name
350 return Patch(name, self.__patch_dir, self.__refs_dir)
352 def get_current_patch(self):
353 """Return a Patch object representing the topmost patch, or
354 None if there is no such patch."""
355 crt = self.get_current()
358 return Patch(crt, self.__patch_dir, self.__refs_dir)
360 def get_current(self):
361 """Return the name of the topmost patch, or None if there is
363 name = self._get_field('current')
369 def get_applied(self):
370 if not os.path.isfile(self.__applied_file):
371 raise StackException, 'Branch "%s" not initialised' % self.__name
372 f = file(self.__applied_file)
373 names = [line.strip() for line in f.readlines()]
377 def get_unapplied(self):
378 if not os.path.isfile(self.__unapplied_file):
379 raise StackException, 'Branch "%s" not initialised' % self.__name
380 f = file(self.__unapplied_file)
381 names = [line.strip() for line in f.readlines()]
385 def get_hidden(self):
386 if not os.path.isfile(self.__hidden_file):
388 f = file(self.__hidden_file)
389 names = [line.strip() for line in f.readlines()]
393 def get_base_file(self):
394 self.__begin_stack_check()
395 return self.__base_file
397 def get_protected(self):
398 return os.path.isfile(os.path.join(self._dir(), 'protected'))
401 protect_file = os.path.join(self._dir(), 'protected')
402 if not os.path.isfile(protect_file):
403 create_empty_file(protect_file)
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)
420 elif 'origin' in git.remotes_list():
421 # FIXME: this is for compatibility only. Should be
422 # dropped when all relevant commands record this info.
425 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
427 def __set_parent_remote(self, remote):
428 value = config.set('branch.%s.remote' % self.__name, remote)
430 def get_parent_branch(self):
431 value = config.get('branch.%s.merge' % self.__name)
434 elif git.rev_parse('heads/origin'):
435 # FIXME: this is for compatibility only. Should be
436 # dropped when all relevant commands record this info.
437 return 'heads/origin'
439 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
441 def __set_parent_branch(self, name):
442 config.set('branch.%s.merge' % self.__name, name)
444 def set_parent(self, remote, localbranch):
446 self.__set_parent_branch(localbranch)
448 self.__set_parent_remote(remote)
450 raise StackException, 'Remote "%s" without a branch cannot be used as parent' % remote
452 def __patch_is_current(self, patch):
453 return patch.get_name() == self.get_current()
455 def patch_applied(self, name):
456 """Return true if the patch exists in the applied list
458 return name in self.get_applied()
460 def patch_unapplied(self, name):
461 """Return true if the patch exists in the unapplied list
463 return name in self.get_unapplied()
465 def patch_hidden(self, name):
466 """Return true if the patch is hidden.
468 return name in self.get_hidden()
470 def patch_exists(self, name):
471 """Return true if there is a patch with the given name, false
473 return self.patch_applied(name) or self.patch_unapplied(name)
475 def __begin_stack_check(self):
476 """Save the current HEAD into .git/refs/heads/base if the stack
479 if len(self.get_applied()) == 0:
480 head = git.get_head()
481 write_string(self.__base_file, head)
483 def __end_stack_check(self):
484 """Remove .git/refs/heads/base if the stack is empty.
485 This warning should never happen
487 if len(self.get_applied()) == 0 \
488 and read_string(self.__base_file) != git.get_head():
489 print 'Warning: stack empty but the HEAD and base are different'
491 def head_top_equal(self):
492 """Return true if the head and the top are the same
494 crt = self.get_current_patch()
496 # we don't care, no patches applied
498 return git.get_head() == crt.get_top()
500 def is_initialised(self):
501 """Checks if series is already initialised
503 return os.path.isdir(self.__patch_dir)
505 def init(self, create_at=False, parent_remote=None, parent_branch=None):
506 """Initialises the stgit series
508 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
510 if os.path.exists(self.__patch_dir):
511 raise StackException, self.__patch_dir + ' already exists'
512 if os.path.exists(self.__refs_dir):
513 raise StackException, self.__refs_dir + ' already exists'
514 if os.path.exists(self.__base_file):
515 raise StackException, self.__base_file + ' already exists'
517 if (create_at!=False):
518 git.create_branch(self.__name, create_at)
520 os.makedirs(self.__patch_dir)
522 self.set_parent(parent_remote, parent_branch)
524 create_dirs(bases_dir)
526 self.create_empty_field('applied')
527 self.create_empty_field('unapplied')
528 self.create_empty_field('description')
529 os.makedirs(os.path.join(self._dir(), 'patches'))
530 os.makedirs(self.__refs_dir)
531 self.__begin_stack_check()
534 """Either convert to use a separate patch directory, or
535 unconvert to place the patches in the same directory with
538 if self.__patch_dir == self._dir():
539 print 'Converting old-style to new-style...',
542 self.__patch_dir = os.path.join(self._dir(), 'patches')
543 os.makedirs(self.__patch_dir)
545 for p in self.get_applied() + self.get_unapplied():
546 src = os.path.join(self._dir(), p)
547 dest = os.path.join(self.__patch_dir, p)
553 print 'Converting new-style to old-style...',
556 for p in self.get_applied() + self.get_unapplied():
557 src = os.path.join(self.__patch_dir, p)
558 dest = os.path.join(self._dir(), p)
561 if not os.listdir(self.__patch_dir):
562 os.rmdir(self.__patch_dir)
565 print 'Patch directory %s is not empty.' % self.__name
567 self.__patch_dir = self._dir()
569 def rename(self, to_name):
572 to_stack = Series(to_name)
574 if to_stack.is_initialised():
575 raise StackException, '"%s" already exists' % to_stack.get_branch()
576 if os.path.exists(to_stack.__base_file):
577 os.remove(to_stack.__base_file)
579 git.rename_branch(self.__name, to_name)
581 if os.path.isdir(self._dir()):
582 rename(os.path.join(self.__base_dir, 'patches'),
583 self.__name, to_stack.__name)
584 if os.path.exists(self.__base_file):
585 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
586 self.__name, to_stack.__name)
587 if os.path.exists(self.__refs_dir):
588 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
589 self.__name, to_stack.__name)
591 self.__init__(to_name)
593 def clone(self, target_series):
597 # allow cloning of branches not under StGIT control
598 base = read_string(self.get_base_file())
600 base = git.get_head()
601 Series(target_series).init(create_at = base)
602 new_series = Series(target_series)
604 # generate an artificial description file
605 new_series.set_description('clone of "%s"' % self.__name)
607 # clone self's entire series as unapplied patches
609 # allow cloning of branches not under StGIT control
610 applied = self.get_applied()
611 unapplied = self.get_unapplied()
612 patches = applied + unapplied
615 patches = applied = unapplied = []
617 patch = self.get_patch(p)
618 new_series.new_patch(p, message = patch.get_description(),
619 can_edit = False, unapplied = True,
620 bottom = patch.get_bottom(),
621 top = patch.get_top(),
622 author_name = patch.get_authname(),
623 author_email = patch.get_authemail(),
624 author_date = patch.get_authdate())
626 # fast forward the cloned series to self's top
627 new_series.forward_patches(applied)
629 def delete(self, force = False):
630 """Deletes an stgit series
632 if self.is_initialised():
633 patches = self.get_unapplied() + self.get_applied()
634 if not force and patches:
635 raise StackException, \
636 'Cannot delete: the series still contains patches'
638 Patch(p, self.__patch_dir, self.__refs_dir).delete()
640 # remove the trash directory
641 for fname in os.listdir(self.__trash_dir):
643 os.rmdir(self.__trash_dir)
645 # FIXME: find a way to get rid of those manual removals
646 # (move functionnality to StgitObject ?)
647 if os.path.exists(self.__applied_file):
648 os.remove(self.__applied_file)
649 if os.path.exists(self.__unapplied_file):
650 os.remove(self.__unapplied_file)
651 if os.path.exists(self.__hidden_file):
652 os.remove(self.__hidden_file)
653 if os.path.exists(self.__current_file):
654 os.remove(self.__current_file)
655 if os.path.exists(self.__descr_file):
656 os.remove(self.__descr_file)
657 if not os.listdir(self.__patch_dir):
658 os.rmdir(self.__patch_dir)
660 print 'Patch directory %s is not empty.' % self.__name
661 if not os.listdir(self._dir()):
662 remove_dirs(os.path.join(self.__base_dir, 'patches'),
665 print 'Series directory %s is not empty.' % self.__name
666 if not os.listdir(self.__refs_dir):
667 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
670 print 'Refs directory %s is not empty.' % self.__refs_dir
672 if os.path.exists(self.__base_file):
673 remove_file_and_dirs(
674 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
676 def refresh_patch(self, files = None, message = None, edit = False,
679 author_name = None, author_email = None,
681 committer_name = None, committer_email = None,
682 backup = False, sign_str = None, log = 'refresh'):
683 """Generates a new commit for the given patch
685 name = self.get_current()
687 raise StackException, 'No patches applied'
689 patch = Patch(name, self.__patch_dir, self.__refs_dir)
691 descr = patch.get_description()
692 if not (message or descr):
698 if not message and edit:
699 descr = edit_file(self, descr.rstrip(), \
700 'Please edit the description for patch "%s" ' \
701 'above.' % name, show_patch)
704 author_name = patch.get_authname()
706 author_email = patch.get_authemail()
708 author_date = patch.get_authdate()
709 if not committer_name:
710 committer_name = patch.get_commname()
711 if not committer_email:
712 committer_email = patch.get_commemail()
715 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
716 committer_name, committer_email)
718 bottom = patch.get_bottom()
720 commit_id = git.commit(files = files,
721 message = descr, parents = [bottom],
722 cache_update = cache_update,
724 author_name = author_name,
725 author_email = author_email,
726 author_date = author_date,
727 committer_name = committer_name,
728 committer_email = committer_email)
730 patch.set_bottom(bottom, backup = backup)
731 patch.set_top(commit_id, backup = backup)
732 patch.set_description(descr)
733 patch.set_authname(author_name)
734 patch.set_authemail(author_email)
735 patch.set_authdate(author_date)
736 patch.set_commname(committer_name)
737 patch.set_commemail(committer_email)
740 self.log_patch(patch, log)
744 def undo_refresh(self):
745 """Undo the patch boundaries changes caused by 'refresh'
747 name = self.get_current()
750 patch = Patch(name, self.__patch_dir, self.__refs_dir)
751 old_bottom = patch.get_old_bottom()
752 old_top = patch.get_old_top()
754 # the bottom of the patch is not changed by refresh. If the
755 # old_bottom is different, there wasn't any previous 'refresh'
756 # command (probably only a 'push')
757 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
758 raise StackException, 'No undo information available'
760 git.reset(tree_id = old_top, check_out = False)
761 if patch.restore_old_boundaries():
762 self.log_patch(patch, 'undo')
764 def new_patch(self, name, message = None, can_edit = True,
765 unapplied = False, show_patch = False,
766 top = None, bottom = None,
767 author_name = None, author_email = None, author_date = None,
768 committer_name = None, committer_email = None,
769 before_existing = False, refresh = True):
770 """Creates a new patch
772 self.__patch_name_valid(name)
774 if self.patch_applied(name) or self.patch_unapplied(name):
775 raise StackException, 'Patch "%s" already exists' % name
777 if not message and can_edit:
778 descr = edit_file(self, None, \
779 'Please enter the description for patch "%s" ' \
780 'above.' % name, show_patch)
784 head = git.get_head()
786 self.__begin_stack_check()
788 patch = Patch(name, self.__patch_dir, self.__refs_dir)
792 patch.set_bottom(bottom)
794 patch.set_bottom(head)
800 patch.set_description(descr)
801 patch.set_authname(author_name)
802 patch.set_authemail(author_email)
803 patch.set_authdate(author_date)
804 patch.set_commname(committer_name)
805 patch.set_commemail(committer_email)
808 self.log_patch(patch, 'new')
810 patches = [patch.get_name()] + self.get_unapplied()
812 f = file(self.__unapplied_file, 'w+')
813 f.writelines([line + '\n' for line in patches])
815 elif before_existing:
816 self.log_patch(patch, 'new')
818 insert_string(self.__applied_file, patch.get_name())
819 if not self.get_current():
820 self.__set_current(name)
822 append_string(self.__applied_file, patch.get_name())
823 self.__set_current(name)
825 self.refresh_patch(cache_update = False, log = 'new')
827 def delete_patch(self, name):
830 self.__patch_name_valid(name)
831 patch = Patch(name, self.__patch_dir, self.__refs_dir)
833 if self.__patch_is_current(patch):
835 elif self.patch_applied(name):
836 raise StackException, 'Cannot remove an applied patch, "%s", ' \
837 'which is not current' % name
838 elif not name in self.get_unapplied():
839 raise StackException, 'Unknown patch "%s"' % name
841 # save the commit id to a trash file
842 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
846 unapplied = self.get_unapplied()
847 unapplied.remove(name)
848 f = file(self.__unapplied_file, 'w+')
849 f.writelines([line + '\n' for line in unapplied])
852 if self.patch_hidden(name):
853 self.unhide_patch(name)
855 self.__begin_stack_check()
857 def forward_patches(self, names):
858 """Try to fast-forward an array of patches.
860 On return, patches in names[0:returned_value] have been pushed on the
861 stack. Apply the rest with push_patch
863 unapplied = self.get_unapplied()
864 self.__begin_stack_check()
870 assert(name in unapplied)
872 patch = Patch(name, self.__patch_dir, self.__refs_dir)
875 bottom = patch.get_bottom()
876 top = patch.get_top()
878 # top != bottom always since we have a commit for each patch
880 # reset the backup information. No logging since the
881 # patch hasn't changed
882 patch.set_bottom(head, backup = True)
883 patch.set_top(top, backup = True)
886 head_tree = git.get_commit(head).get_tree()
887 bottom_tree = git.get_commit(bottom).get_tree()
888 if head_tree == bottom_tree:
889 # We must just reparent this patch and create a new commit
891 descr = patch.get_description()
892 author_name = patch.get_authname()
893 author_email = patch.get_authemail()
894 author_date = patch.get_authdate()
895 committer_name = patch.get_commname()
896 committer_email = patch.get_commemail()
898 top_tree = git.get_commit(top).get_tree()
900 top = git.commit(message = descr, parents = [head],
901 cache_update = False,
904 author_name = author_name,
905 author_email = author_email,
906 author_date = author_date,
907 committer_name = committer_name,
908 committer_email = committer_email)
910 patch.set_bottom(head, backup = True)
911 patch.set_top(top, backup = True)
913 self.log_patch(patch, 'push(f)')
916 # stop the fast-forwarding, must do a real merge
920 unapplied.remove(name)
927 append_strings(self.__applied_file, names[0:forwarded])
929 f = file(self.__unapplied_file, 'w+')
930 f.writelines([line + '\n' for line in unapplied])
933 self.__set_current(name)
937 def merged_patches(self, names):
938 """Test which patches were merged upstream by reverse-applying
939 them in reverse order. The function returns the list of
940 patches detected to have been applied. The state of the tree
941 is restored to the original one
943 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
949 if git.apply_diff(p.get_top(), p.get_bottom()):
950 merged.append(p.get_name())
957 def push_patch(self, name, empty = False):
958 """Pushes a patch on the stack
960 unapplied = self.get_unapplied()
961 assert(name in unapplied)
963 self.__begin_stack_check()
965 patch = Patch(name, self.__patch_dir, self.__refs_dir)
967 head = git.get_head()
968 bottom = patch.get_bottom()
969 top = patch.get_top()
974 # top != bottom always since we have a commit for each patch
976 # just make an empty patch (top = bottom = HEAD). This
977 # option is useful to allow undoing already merged
978 # patches. The top is updated by refresh_patch since we
979 # need an empty commit
980 patch.set_bottom(head, backup = True)
981 patch.set_top(head, backup = True)
984 # reset the backup information. No need for logging
985 patch.set_bottom(bottom, backup = True)
986 patch.set_top(top, backup = True)
990 # new patch needs to be refreshed.
991 # The current patch is empty after merge.
992 patch.set_bottom(head, backup = True)
993 patch.set_top(head, backup = True)
995 # Try the fast applying first. If this fails, fall back to the
997 if not git.apply_diff(bottom, top):
998 # if git.apply_diff() fails, the patch requires a diff3
999 # merge and can be reported as modified
1002 # merge can fail but the patch needs to be pushed
1004 git.merge(bottom, head, top, recursive = True)
1005 except git.GitException, ex:
1006 print >> sys.stderr, \
1007 'The merge failed during "push". ' \
1008 'Use "refresh" after fixing the conflicts'
1010 append_string(self.__applied_file, name)
1012 unapplied.remove(name)
1013 f = file(self.__unapplied_file, 'w+')
1014 f.writelines([line + '\n' for line in unapplied])
1017 self.__set_current(name)
1019 # head == bottom case doesn't need to refresh the patch
1020 if empty or head != bottom:
1022 # if the merge was OK and no conflicts, just refresh the patch
1023 # The GIT cache was already updated by the merge operation
1028 self.refresh_patch(cache_update = False, log = log)
1030 # we store the correctly merged files only for
1031 # tracking the conflict history. Note that the
1032 # git.merge() operations shouls always leave the index
1033 # in a valid state (i.e. only stage 0 files)
1034 self.refresh_patch(cache_update = False, log = 'push(c)')
1035 raise StackException, str(ex)
1039 def undo_push(self):
1040 name = self.get_current()
1043 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1044 old_bottom = patch.get_old_bottom()
1045 old_top = patch.get_old_top()
1047 # the top of the patch is changed by a push operation only
1048 # together with the bottom (otherwise the top was probably
1049 # modified by 'refresh'). If they are both unchanged, there
1050 # was a fast forward
1051 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1052 raise StackException, 'No undo information available'
1055 self.pop_patch(name)
1056 ret = patch.restore_old_boundaries()
1058 self.log_patch(patch, 'undo')
1062 def pop_patch(self, name, keep = False):
1063 """Pops the top patch from the stack
1065 applied = self.get_applied()
1067 assert(name in applied)
1069 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1071 # only keep the local changes
1072 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1073 raise StackException, \
1074 'Failed to pop patches while preserving the local changes'
1076 git.switch(patch.get_bottom(), keep)
1078 # save the new applied list
1079 idx = applied.index(name) + 1
1081 popped = applied[:idx]
1083 unapplied = popped + self.get_unapplied()
1085 f = file(self.__unapplied_file, 'w+')
1086 f.writelines([line + '\n' for line in unapplied])
1092 f = file(self.__applied_file, 'w+')
1093 f.writelines([line + '\n' for line in applied])
1097 self.__set_current(None)
1099 self.__set_current(applied[-1])
1101 self.__end_stack_check()
1103 def empty_patch(self, name):
1104 """Returns True if the patch is empty
1106 self.__patch_name_valid(name)
1107 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1108 bottom = patch.get_bottom()
1109 top = patch.get_top()
1113 elif git.get_commit(top).get_tree() \
1114 == git.get_commit(bottom).get_tree():
1119 def rename_patch(self, oldname, newname):
1120 self.__patch_name_valid(newname)
1122 applied = self.get_applied()
1123 unapplied = self.get_unapplied()
1125 if oldname == newname:
1126 raise StackException, '"To" name and "from" name are the same'
1128 if newname in applied or newname in unapplied:
1129 raise StackException, 'Patch "%s" already exists' % newname
1131 if self.patch_hidden(oldname):
1132 self.unhide_patch(oldname)
1133 self.hide_patch(newname)
1135 if oldname in unapplied:
1136 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1137 unapplied[unapplied.index(oldname)] = newname
1139 f = file(self.__unapplied_file, 'w+')
1140 f.writelines([line + '\n' for line in unapplied])
1142 elif oldname in applied:
1143 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1144 if oldname == self.get_current():
1145 self.__set_current(newname)
1147 applied[applied.index(oldname)] = newname
1149 f = file(self.__applied_file, 'w+')
1150 f.writelines([line + '\n' for line in applied])
1153 raise StackException, 'Unknown patch "%s"' % oldname
1155 def log_patch(self, patch, message):
1156 """Generate a log commit for a patch
1158 top = git.get_commit(patch.get_top())
1159 msg = '%s\t%s' % (message, top.get_id_hash())
1161 old_log = patch.get_log()
1167 log = git.commit(message = msg, parents = parents,
1168 cache_update = False, tree_id = top.get_tree(),
1172 def hide_patch(self, name):
1173 """Add the patch to the hidden list.
1175 if not self.patch_exists(name):
1176 raise StackException, 'Unknown patch "%s"' % name
1177 elif self.patch_hidden(name):
1178 raise StackException, 'Patch "%s" already hidden' % name
1180 append_string(self.__hidden_file, name)
1182 def unhide_patch(self, name):
1183 """Add the patch to the hidden list.
1185 if not self.patch_exists(name):
1186 raise StackException, 'Unknown patch "%s"' % name
1187 hidden = self.get_hidden()
1188 if not name in hidden:
1189 raise StackException, 'Patch "%s" not hidden' % name
1193 f = file(self.__hidden_file, 'w+')
1194 f.writelines([line + '\n' for line in hidden])