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 print 'Notice: no parent remote declared for stack "%s", defaulting to "origin".' \
422 'Consider setting "branch.%s.remote" with "git repo-config".' \
423 % (self.__name, self.__name)
426 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
428 def __set_parent_remote(self, remote):
429 value = config.set('branch.%s.remote' % self.__name, remote)
431 def get_parent_branch(self):
432 value = config.get('branch.%s.merge' % self.__name)
435 elif git.rev_parse('heads/origin'):
436 print 'Notice: no parent branch declared for stack "%s", defaulting to "heads/origin".' \
437 'Consider setting "branch.%s.merge" with "git repo-config".' \
438 % (self.__name, self.__name)
439 return 'heads/origin'
441 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
443 def __set_parent_branch(self, name):
444 config.set('branch.%s.merge' % self.__name, name)
446 def set_parent(self, remote, localbranch):
448 self.__set_parent_branch(localbranch)
450 self.__set_parent_remote(remote)
452 raise StackException, 'Remote "%s" without a branch cannot be used as parent' % remote
454 def __patch_is_current(self, patch):
455 return patch.get_name() == self.get_current()
457 def patch_applied(self, name):
458 """Return true if the patch exists in the applied list
460 return name in self.get_applied()
462 def patch_unapplied(self, name):
463 """Return true if the patch exists in the unapplied list
465 return name in self.get_unapplied()
467 def patch_hidden(self, name):
468 """Return true if the patch is hidden.
470 return name in self.get_hidden()
472 def patch_exists(self, name):
473 """Return true if there is a patch with the given name, false
475 return self.patch_applied(name) or self.patch_unapplied(name)
477 def __begin_stack_check(self):
478 """Save the current HEAD into .git/refs/heads/base if the stack
481 if len(self.get_applied()) == 0:
482 head = git.get_head()
483 write_string(self.__base_file, head)
485 def __end_stack_check(self):
486 """Remove .git/refs/heads/base if the stack is empty.
487 This warning should never happen
489 if len(self.get_applied()) == 0 \
490 and read_string(self.__base_file) != git.get_head():
491 print 'Warning: stack empty but the HEAD and base are different'
493 def head_top_equal(self):
494 """Return true if the head and the top are the same
496 crt = self.get_current_patch()
498 # we don't care, no patches applied
500 return git.get_head() == crt.get_top()
502 def is_initialised(self):
503 """Checks if series is already initialised
505 return os.path.isdir(self.__patch_dir)
507 def init(self, create_at=False, parent_remote=None, parent_branch=None):
508 """Initialises the stgit series
510 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
512 if os.path.exists(self.__patch_dir):
513 raise StackException, self.__patch_dir + ' already exists'
514 if os.path.exists(self.__refs_dir):
515 raise StackException, self.__refs_dir + ' already exists'
516 if os.path.exists(self.__base_file):
517 raise StackException, self.__base_file + ' already exists'
519 if (create_at!=False):
520 git.create_branch(self.__name, create_at)
522 os.makedirs(self.__patch_dir)
524 self.set_parent(parent_remote, parent_branch)
526 create_dirs(bases_dir)
528 self.create_empty_field('applied')
529 self.create_empty_field('unapplied')
530 self.create_empty_field('description')
531 os.makedirs(os.path.join(self._dir(), 'patches'))
532 os.makedirs(self.__refs_dir)
533 self.__begin_stack_check()
536 """Either convert to use a separate patch directory, or
537 unconvert to place the patches in the same directory with
540 if self.__patch_dir == self._dir():
541 print 'Converting old-style to new-style...',
544 self.__patch_dir = os.path.join(self._dir(), 'patches')
545 os.makedirs(self.__patch_dir)
547 for p in self.get_applied() + self.get_unapplied():
548 src = os.path.join(self._dir(), p)
549 dest = os.path.join(self.__patch_dir, p)
555 print 'Converting new-style to old-style...',
558 for p in self.get_applied() + self.get_unapplied():
559 src = os.path.join(self.__patch_dir, p)
560 dest = os.path.join(self._dir(), p)
563 if not os.listdir(self.__patch_dir):
564 os.rmdir(self.__patch_dir)
567 print 'Patch directory %s is not empty.' % self.__name
569 self.__patch_dir = self._dir()
571 def rename(self, to_name):
574 to_stack = Series(to_name)
576 if to_stack.is_initialised():
577 raise StackException, '"%s" already exists' % to_stack.get_branch()
578 if os.path.exists(to_stack.__base_file):
579 os.remove(to_stack.__base_file)
581 git.rename_branch(self.__name, to_name)
583 if os.path.isdir(self._dir()):
584 rename(os.path.join(self.__base_dir, 'patches'),
585 self.__name, to_stack.__name)
586 if os.path.exists(self.__base_file):
587 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
588 self.__name, to_stack.__name)
589 if os.path.exists(self.__refs_dir):
590 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
591 self.__name, to_stack.__name)
593 # Rename the config section
594 config.rename_section("branch.%s" % self.__name,
595 "branch.%s" % to_name)
597 self.__init__(to_name)
599 def clone(self, target_series):
603 # allow cloning of branches not under StGIT control
604 base = read_string(self.get_base_file())
606 base = git.get_head()
607 Series(target_series).init(create_at = base)
608 new_series = Series(target_series)
610 # generate an artificial description file
611 new_series.set_description('clone of "%s"' % self.__name)
613 # clone self's entire series as unapplied patches
615 # allow cloning of branches not under StGIT control
616 applied = self.get_applied()
617 unapplied = self.get_unapplied()
618 patches = applied + unapplied
621 patches = applied = unapplied = []
623 patch = self.get_patch(p)
624 new_series.new_patch(p, message = patch.get_description(),
625 can_edit = False, unapplied = True,
626 bottom = patch.get_bottom(),
627 top = patch.get_top(),
628 author_name = patch.get_authname(),
629 author_email = patch.get_authemail(),
630 author_date = patch.get_authdate())
632 # fast forward the cloned series to self's top
633 new_series.forward_patches(applied)
635 def delete(self, force = False):
636 """Deletes an stgit series
638 if self.is_initialised():
639 patches = self.get_unapplied() + self.get_applied()
640 if not force and patches:
641 raise StackException, \
642 'Cannot delete: the series still contains patches'
644 Patch(p, self.__patch_dir, self.__refs_dir).delete()
646 # remove the trash directory
647 for fname in os.listdir(self.__trash_dir):
649 os.rmdir(self.__trash_dir)
651 # FIXME: find a way to get rid of those manual removals
652 # (move functionnality to StgitObject ?)
653 if os.path.exists(self.__applied_file):
654 os.remove(self.__applied_file)
655 if os.path.exists(self.__unapplied_file):
656 os.remove(self.__unapplied_file)
657 if os.path.exists(self.__hidden_file):
658 os.remove(self.__hidden_file)
659 if os.path.exists(self.__current_file):
660 os.remove(self.__current_file)
661 if os.path.exists(self.__descr_file):
662 os.remove(self.__descr_file)
663 if not os.listdir(self.__patch_dir):
664 os.rmdir(self.__patch_dir)
666 print 'Patch directory %s is not empty.' % self.__name
667 if not os.listdir(self._dir()):
668 remove_dirs(os.path.join(self.__base_dir, 'patches'),
671 print 'Series directory %s is not empty.' % self.__name
672 if not os.listdir(self.__refs_dir):
673 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
676 print 'Refs directory %s is not empty.' % self.__refs_dir
678 if os.path.exists(self.__base_file):
679 remove_file_and_dirs(
680 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
682 def refresh_patch(self, files = None, message = None, edit = False,
685 author_name = None, author_email = None,
687 committer_name = None, committer_email = None,
688 backup = False, sign_str = None, log = 'refresh'):
689 """Generates a new commit for the given patch
691 name = self.get_current()
693 raise StackException, 'No patches applied'
695 patch = Patch(name, self.__patch_dir, self.__refs_dir)
697 descr = patch.get_description()
698 if not (message or descr):
704 if not message and edit:
705 descr = edit_file(self, descr.rstrip(), \
706 'Please edit the description for patch "%s" ' \
707 'above.' % name, show_patch)
710 author_name = patch.get_authname()
712 author_email = patch.get_authemail()
714 author_date = patch.get_authdate()
715 if not committer_name:
716 committer_name = patch.get_commname()
717 if not committer_email:
718 committer_email = patch.get_commemail()
721 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
722 committer_name, committer_email)
724 bottom = patch.get_bottom()
726 commit_id = git.commit(files = files,
727 message = descr, parents = [bottom],
728 cache_update = cache_update,
730 author_name = author_name,
731 author_email = author_email,
732 author_date = author_date,
733 committer_name = committer_name,
734 committer_email = committer_email)
736 patch.set_bottom(bottom, backup = backup)
737 patch.set_top(commit_id, backup = backup)
738 patch.set_description(descr)
739 patch.set_authname(author_name)
740 patch.set_authemail(author_email)
741 patch.set_authdate(author_date)
742 patch.set_commname(committer_name)
743 patch.set_commemail(committer_email)
746 self.log_patch(patch, log)
750 def undo_refresh(self):
751 """Undo the patch boundaries changes caused by 'refresh'
753 name = self.get_current()
756 patch = Patch(name, self.__patch_dir, self.__refs_dir)
757 old_bottom = patch.get_old_bottom()
758 old_top = patch.get_old_top()
760 # the bottom of the patch is not changed by refresh. If the
761 # old_bottom is different, there wasn't any previous 'refresh'
762 # command (probably only a 'push')
763 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
764 raise StackException, 'No undo information available'
766 git.reset(tree_id = old_top, check_out = False)
767 if patch.restore_old_boundaries():
768 self.log_patch(patch, 'undo')
770 def new_patch(self, name, message = None, can_edit = True,
771 unapplied = False, show_patch = False,
772 top = None, bottom = None,
773 author_name = None, author_email = None, author_date = None,
774 committer_name = None, committer_email = None,
775 before_existing = False, refresh = True):
776 """Creates a new patch
778 self.__patch_name_valid(name)
780 if self.patch_applied(name) or self.patch_unapplied(name):
781 raise StackException, 'Patch "%s" already exists' % name
783 if not message and can_edit:
784 descr = edit_file(self, None, \
785 'Please enter the description for patch "%s" ' \
786 'above.' % name, show_patch)
790 head = git.get_head()
792 self.__begin_stack_check()
794 patch = Patch(name, self.__patch_dir, self.__refs_dir)
798 patch.set_bottom(bottom)
800 patch.set_bottom(head)
806 patch.set_description(descr)
807 patch.set_authname(author_name)
808 patch.set_authemail(author_email)
809 patch.set_authdate(author_date)
810 patch.set_commname(committer_name)
811 patch.set_commemail(committer_email)
814 self.log_patch(patch, 'new')
816 patches = [patch.get_name()] + self.get_unapplied()
818 f = file(self.__unapplied_file, 'w+')
819 f.writelines([line + '\n' for line in patches])
821 elif before_existing:
822 self.log_patch(patch, 'new')
824 insert_string(self.__applied_file, patch.get_name())
825 if not self.get_current():
826 self.__set_current(name)
828 append_string(self.__applied_file, patch.get_name())
829 self.__set_current(name)
831 self.refresh_patch(cache_update = False, log = 'new')
833 def delete_patch(self, name):
836 self.__patch_name_valid(name)
837 patch = Patch(name, self.__patch_dir, self.__refs_dir)
839 if self.__patch_is_current(patch):
841 elif self.patch_applied(name):
842 raise StackException, 'Cannot remove an applied patch, "%s", ' \
843 'which is not current' % name
844 elif not name in self.get_unapplied():
845 raise StackException, 'Unknown patch "%s"' % name
847 # save the commit id to a trash file
848 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
852 unapplied = self.get_unapplied()
853 unapplied.remove(name)
854 f = file(self.__unapplied_file, 'w+')
855 f.writelines([line + '\n' for line in unapplied])
858 if self.patch_hidden(name):
859 self.unhide_patch(name)
861 self.__begin_stack_check()
863 def forward_patches(self, names):
864 """Try to fast-forward an array of patches.
866 On return, patches in names[0:returned_value] have been pushed on the
867 stack. Apply the rest with push_patch
869 unapplied = self.get_unapplied()
870 self.__begin_stack_check()
876 assert(name in unapplied)
878 patch = Patch(name, self.__patch_dir, self.__refs_dir)
881 bottom = patch.get_bottom()
882 top = patch.get_top()
884 # top != bottom always since we have a commit for each patch
886 # reset the backup information. No logging since the
887 # patch hasn't changed
888 patch.set_bottom(head, backup = True)
889 patch.set_top(top, backup = True)
892 head_tree = git.get_commit(head).get_tree()
893 bottom_tree = git.get_commit(bottom).get_tree()
894 if head_tree == bottom_tree:
895 # We must just reparent this patch and create a new commit
897 descr = patch.get_description()
898 author_name = patch.get_authname()
899 author_email = patch.get_authemail()
900 author_date = patch.get_authdate()
901 committer_name = patch.get_commname()
902 committer_email = patch.get_commemail()
904 top_tree = git.get_commit(top).get_tree()
906 top = git.commit(message = descr, parents = [head],
907 cache_update = False,
910 author_name = author_name,
911 author_email = author_email,
912 author_date = author_date,
913 committer_name = committer_name,
914 committer_email = committer_email)
916 patch.set_bottom(head, backup = True)
917 patch.set_top(top, backup = True)
919 self.log_patch(patch, 'push(f)')
922 # stop the fast-forwarding, must do a real merge
926 unapplied.remove(name)
933 append_strings(self.__applied_file, names[0:forwarded])
935 f = file(self.__unapplied_file, 'w+')
936 f.writelines([line + '\n' for line in unapplied])
939 self.__set_current(name)
943 def merged_patches(self, names):
944 """Test which patches were merged upstream by reverse-applying
945 them in reverse order. The function returns the list of
946 patches detected to have been applied. The state of the tree
947 is restored to the original one
949 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
955 if git.apply_diff(p.get_top(), p.get_bottom()):
956 merged.append(p.get_name())
963 def push_patch(self, name, empty = False):
964 """Pushes a patch on the stack
966 unapplied = self.get_unapplied()
967 assert(name in unapplied)
969 self.__begin_stack_check()
971 patch = Patch(name, self.__patch_dir, self.__refs_dir)
973 head = git.get_head()
974 bottom = patch.get_bottom()
975 top = patch.get_top()
980 # top != bottom always since we have a commit for each patch
982 # just make an empty patch (top = bottom = HEAD). This
983 # option is useful to allow undoing already merged
984 # patches. The top is updated by refresh_patch since we
985 # need an empty commit
986 patch.set_bottom(head, backup = True)
987 patch.set_top(head, backup = True)
990 # reset the backup information. No need for logging
991 patch.set_bottom(bottom, backup = True)
992 patch.set_top(top, backup = True)
996 # new patch needs to be refreshed.
997 # The current patch is empty after merge.
998 patch.set_bottom(head, backup = True)
999 patch.set_top(head, backup = True)
1001 # Try the fast applying first. If this fails, fall back to the
1003 if not git.apply_diff(bottom, top):
1004 # if git.apply_diff() fails, the patch requires a diff3
1005 # merge and can be reported as modified
1008 # merge can fail but the patch needs to be pushed
1010 git.merge(bottom, head, top, recursive = True)
1011 except git.GitException, ex:
1012 print >> sys.stderr, \
1013 'The merge failed during "push". ' \
1014 'Use "refresh" after fixing the conflicts'
1016 append_string(self.__applied_file, name)
1018 unapplied.remove(name)
1019 f = file(self.__unapplied_file, 'w+')
1020 f.writelines([line + '\n' for line in unapplied])
1023 self.__set_current(name)
1025 # head == bottom case doesn't need to refresh the patch
1026 if empty or head != bottom:
1028 # if the merge was OK and no conflicts, just refresh the patch
1029 # The GIT cache was already updated by the merge operation
1034 self.refresh_patch(cache_update = False, log = log)
1036 # we store the correctly merged files only for
1037 # tracking the conflict history. Note that the
1038 # git.merge() operations shouls always leave the index
1039 # in a valid state (i.e. only stage 0 files)
1040 self.refresh_patch(cache_update = False, log = 'push(c)')
1041 raise StackException, str(ex)
1045 def undo_push(self):
1046 name = self.get_current()
1049 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1050 old_bottom = patch.get_old_bottom()
1051 old_top = patch.get_old_top()
1053 # the top of the patch is changed by a push operation only
1054 # together with the bottom (otherwise the top was probably
1055 # modified by 'refresh'). If they are both unchanged, there
1056 # was a fast forward
1057 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1058 raise StackException, 'No undo information available'
1061 self.pop_patch(name)
1062 ret = patch.restore_old_boundaries()
1064 self.log_patch(patch, 'undo')
1068 def pop_patch(self, name, keep = False):
1069 """Pops the top patch from the stack
1071 applied = self.get_applied()
1073 assert(name in applied)
1075 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1077 # only keep the local changes
1078 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1079 raise StackException, \
1080 'Failed to pop patches while preserving the local changes'
1082 git.switch(patch.get_bottom(), keep)
1084 # save the new applied list
1085 idx = applied.index(name) + 1
1087 popped = applied[:idx]
1089 unapplied = popped + self.get_unapplied()
1091 f = file(self.__unapplied_file, 'w+')
1092 f.writelines([line + '\n' for line in unapplied])
1098 f = file(self.__applied_file, 'w+')
1099 f.writelines([line + '\n' for line in applied])
1103 self.__set_current(None)
1105 self.__set_current(applied[-1])
1107 self.__end_stack_check()
1109 def empty_patch(self, name):
1110 """Returns True if the patch is empty
1112 self.__patch_name_valid(name)
1113 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1114 bottom = patch.get_bottom()
1115 top = patch.get_top()
1119 elif git.get_commit(top).get_tree() \
1120 == git.get_commit(bottom).get_tree():
1125 def rename_patch(self, oldname, newname):
1126 self.__patch_name_valid(newname)
1128 applied = self.get_applied()
1129 unapplied = self.get_unapplied()
1131 if oldname == newname:
1132 raise StackException, '"To" name and "from" name are the same'
1134 if newname in applied or newname in unapplied:
1135 raise StackException, 'Patch "%s" already exists' % newname
1137 if self.patch_hidden(oldname):
1138 self.unhide_patch(oldname)
1139 self.hide_patch(newname)
1141 if oldname in unapplied:
1142 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1143 unapplied[unapplied.index(oldname)] = newname
1145 f = file(self.__unapplied_file, 'w+')
1146 f.writelines([line + '\n' for line in unapplied])
1148 elif oldname in applied:
1149 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1150 if oldname == self.get_current():
1151 self.__set_current(newname)
1153 applied[applied.index(oldname)] = newname
1155 f = file(self.__applied_file, 'w+')
1156 f.writelines([line + '\n' for line in applied])
1159 raise StackException, 'Unknown patch "%s"' % oldname
1161 def log_patch(self, patch, message):
1162 """Generate a log commit for a patch
1164 top = git.get_commit(patch.get_top())
1165 msg = '%s\t%s' % (message, top.get_id_hash())
1167 old_log = patch.get_log()
1173 log = git.commit(message = msg, parents = parents,
1174 cache_update = False, tree_id = top.get_tree(),
1178 def hide_patch(self, name):
1179 """Add the patch to the hidden list.
1181 if not self.patch_exists(name):
1182 raise StackException, 'Unknown patch "%s"' % name
1183 elif self.patch_hidden(name):
1184 raise StackException, 'Patch "%s" already hidden' % name
1186 append_string(self.__hidden_file, name)
1188 def unhide_patch(self, name):
1189 """Add the patch to the hidden list.
1191 if not self.patch_exists(name):
1192 raise StackException, 'Unknown patch "%s"' % name
1193 hidden = self.get_hidden()
1194 if not name in hidden:
1195 raise StackException, 'Patch "%s" not hidden' % name
1199 f = file(self.__hidden_file, 'w+')
1200 f.writelines([line + '\n' for line in hidden])