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:'
112 """An object with stgit-like properties stored as files in a directory
114 def _set_dir(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)
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):
141 class Patch(StgitObject):
142 """Basic patch implementation
144 def __init__(self, name, series_dir, refs_dir):
145 self.__series_dir = series_dir
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')
154 os.mkdir(self._dir())
155 self.create_empty_field('bottom')
156 self.create_empty_field('top')
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)
169 def rename(self, newname):
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):
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):
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')
211 return self._get_field('top')
213 def set_top(self, value, backup = False):
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')
225 self._set_field('bottom', bottom)
226 self._set_field('top', top)
227 self.__update_top_ref(top)
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)
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.
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',
295 self.__applied_file = os.path.join(self._dir(), 'applied')
296 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
297 self.__hidden_file = os.path.join(self._dir(), 'hidden')
298 self.__current_file = os.path.join(self._dir(), 'current')
299 self.__descr_file = os.path.join(self._dir(), 'description')
301 # where this series keeps its patches
302 self.__patch_dir = os.path.join(self._dir(), 'patches')
303 if not os.path.isdir(self.__patch_dir):
304 self.__patch_dir = self._dir()
306 # if no __refs_dir, create and populate it (upgrade old repositories)
307 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
308 os.makedirs(self.__refs_dir)
309 for patch in self.get_applied() + self.get_unapplied():
310 self.get_patch(patch).update_top_ref()
313 self.__trash_dir = os.path.join(self._dir(), 'trash')
314 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
315 os.makedirs(self.__trash_dir)
317 def __patch_name_valid(self, name):
318 """Raise an exception if the patch name is not valid.
320 if not name or re.search('[^\w.-]', name):
321 raise StackException, 'Invalid patch name: "%s"' % name
323 def get_branch(self):
324 """Return the branch name for the Series object
328 def __set_current(self, name):
329 """Sets the topmost patch
331 self._set_field('current', name)
333 def get_patch(self, name):
334 """Return a Patch object for the given name
336 return Patch(name, self.__patch_dir, self.__refs_dir)
338 def get_current_patch(self):
339 """Return a Patch object representing the topmost patch, or
340 None if there is no such patch."""
341 crt = self.get_current()
344 return Patch(crt, self.__patch_dir, self.__refs_dir)
346 def get_current(self):
347 """Return the name of the topmost patch, or None if there is
349 name = self._get_field('current')
355 def get_applied(self):
356 if not os.path.isfile(self.__applied_file):
357 raise StackException, 'Branch "%s" not initialised' % self.__name
358 f = file(self.__applied_file)
359 names = [line.strip() for line in f.readlines()]
363 def get_unapplied(self):
364 if not os.path.isfile(self.__unapplied_file):
365 raise StackException, 'Branch "%s" not initialised' % self.__name
366 f = file(self.__unapplied_file)
367 names = [line.strip() for line in f.readlines()]
371 def get_hidden(self):
372 if not os.path.isfile(self.__hidden_file):
374 f = file(self.__hidden_file)
375 names = [line.strip() for line in f.readlines()]
380 # Return the parent of the bottommost patch, if there is one.
381 if os.path.isfile(self.__applied_file):
382 bottommost = file(self.__applied_file).readline().strip()
384 return self.get_patch(bottommost).get_bottom()
385 # No bottommost patch, so just return HEAD
386 return git.get_head()
389 """Return the head of the branch
391 crt = self.get_current_patch()
395 return self.get_base()
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", ' \
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)
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)
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'
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 '.'
456 self.__set_parent_remote(recordremote)
457 self.__set_parent_branch(localbranch)
458 # We'll enforce this later
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
483 return self.patch_applied(name) or self.patch_unapplied(name)
485 def head_top_equal(self):
486 """Return true if the head and the top are the same
488 crt = self.get_current_patch()
490 # we don't care, no patches applied
492 return git.get_head() == crt.get_top()
494 def is_initialised(self):
495 """Checks if series is already initialised
497 return os.path.isdir(self.__patch_dir)
499 def init(self, create_at=False, parent_remote=None, parent_branch=None):
500 """Initialises the stgit series
502 if os.path.exists(self.__patch_dir):
503 raise StackException, self.__patch_dir + ' already exists'
504 if os.path.exists(self.__refs_dir):
505 raise StackException, self.__refs_dir + ' already exists'
507 if (create_at!=False):
508 git.create_branch(self.__name, create_at)
510 os.makedirs(self.__patch_dir)
512 self.set_parent(parent_remote, parent_branch)
514 self.create_empty_field('applied')
515 self.create_empty_field('unapplied')
516 self.create_empty_field('description')
517 os.makedirs(os.path.join(self._dir(), 'patches'))
518 os.makedirs(self.__refs_dir)
519 self._set_field('orig-base', git.get_head())
522 """Either convert to use a separate patch directory, or
523 unconvert to place the patches in the same directory with
526 if self.__patch_dir == self._dir():
527 print 'Converting old-style to new-style...',
530 self.__patch_dir = os.path.join(self._dir(), 'patches')
531 os.makedirs(self.__patch_dir)
533 for p in self.get_applied() + self.get_unapplied():
534 src = os.path.join(self._dir(), p)
535 dest = os.path.join(self.__patch_dir, p)
541 print 'Converting new-style to old-style...',
544 for p in self.get_applied() + self.get_unapplied():
545 src = os.path.join(self.__patch_dir, p)
546 dest = os.path.join(self._dir(), p)
549 if not os.listdir(self.__patch_dir):
550 os.rmdir(self.__patch_dir)
553 print 'Patch directory %s is not empty.' % self.__patch_dir
555 self.__patch_dir = self._dir()
557 def rename(self, to_name):
560 to_stack = Series(to_name)
562 if to_stack.is_initialised():
563 raise StackException, '"%s" already exists' % to_stack.get_branch()
565 git.rename_branch(self.__name, to_name)
567 if os.path.isdir(self._dir()):
568 rename(os.path.join(self.__base_dir, 'patches'),
569 self.__name, to_stack.__name)
570 if os.path.exists(self.__refs_dir):
571 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
572 self.__name, to_stack.__name)
574 # Rename the config section
575 config.rename_section("branch.%s" % self.__name,
576 "branch.%s" % to_name)
578 self.__init__(to_name)
580 def clone(self, target_series):
584 # allow cloning of branches not under StGIT control
585 base = self.get_base()
587 base = git.get_head()
588 Series(target_series).init(create_at = base)
589 new_series = Series(target_series)
591 # generate an artificial description file
592 new_series.set_description('clone of "%s"' % self.__name)
594 # clone self's entire series as unapplied patches
596 # allow cloning of branches not under StGIT control
597 applied = self.get_applied()
598 unapplied = self.get_unapplied()
599 patches = applied + unapplied
602 patches = applied = unapplied = []
604 patch = self.get_patch(p)
605 new_series.new_patch(p, message = patch.get_description(),
606 can_edit = False, unapplied = True,
607 bottom = patch.get_bottom(),
608 top = patch.get_top(),
609 author_name = patch.get_authname(),
610 author_email = patch.get_authemail(),
611 author_date = patch.get_authdate())
613 # fast forward the cloned series to self's top
614 new_series.forward_patches(applied)
616 # Clone parent informations
617 value = config.get('branch.%s.remote' % self.__name)
619 config.set('branch.%s.remote' % target_series, value)
621 value = config.get('branch.%s.merge' % self.__name)
623 config.set('branch.%s.merge' % target_series, value)
625 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
627 config.set('branch.%s.stgit.parentbranch' % target_series, value)
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 functionality 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 os.path.exists(self._dir()+'/orig-base'):
658 os.remove(self._dir()+'/orig-base')
660 if not os.listdir(self.__patch_dir):
661 os.rmdir(self.__patch_dir)
663 print 'Patch directory %s is not empty.' % self.__patch_dir
666 os.removedirs(self._dir())
668 raise StackException, 'Series directory %s is not empty.' % self._dir()
671 os.removedirs(self.__refs_dir)
673 print 'Refs directory %s is not empty.' % self.__refs_dir
675 # Cleanup parent informations
676 # FIXME: should one day make use of git-config --section-remove,
677 # scheduled for 1.5.1
678 config.unset('branch.%s.remote' % self.__name)
679 config.unset('branch.%s.merge' % self.__name)
680 config.unset('branch.%s.stgit.parentbranch' % 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 = descr.rstrip()
722 if descr.find("\nSigned-off-by:") < 0 \
723 and descr.find("\nAcked-by:") < 0:
726 descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
727 committer_name, committer_email)
729 bottom = patch.get_bottom()
731 commit_id = git.commit(files = files,
732 message = descr, parents = [bottom],
733 cache_update = cache_update,
735 author_name = author_name,
736 author_email = author_email,
737 author_date = author_date,
738 committer_name = committer_name,
739 committer_email = committer_email)
741 patch.set_bottom(bottom, backup = backup)
742 patch.set_top(commit_id, backup = backup)
743 patch.set_description(descr)
744 patch.set_authname(author_name)
745 patch.set_authemail(author_email)
746 patch.set_authdate(author_date)
747 patch.set_commname(committer_name)
748 patch.set_commemail(committer_email)
751 self.log_patch(patch, log)
755 def undo_refresh(self):
756 """Undo the patch boundaries changes caused by 'refresh'
758 name = self.get_current()
761 patch = Patch(name, self.__patch_dir, self.__refs_dir)
762 old_bottom = patch.get_old_bottom()
763 old_top = patch.get_old_top()
765 # the bottom of the patch is not changed by refresh. If the
766 # old_bottom is different, there wasn't any previous 'refresh'
767 # command (probably only a 'push')
768 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
769 raise StackException, 'No undo information available'
771 git.reset(tree_id = old_top, check_out = False)
772 if patch.restore_old_boundaries():
773 self.log_patch(patch, 'undo')
775 def new_patch(self, name, message = None, can_edit = True,
776 unapplied = False, show_patch = False,
777 top = None, bottom = None,
778 author_name = None, author_email = None, author_date = None,
779 committer_name = None, committer_email = None,
780 before_existing = False, refresh = True):
781 """Creates a new patch
783 self.__patch_name_valid(name)
785 if self.patch_applied(name) or self.patch_unapplied(name):
786 raise StackException, 'Patch "%s" already exists' % name
788 if not message and can_edit:
789 descr = edit_file(self, None, \
790 'Please enter the description for patch "%s" ' \
791 'above.' % name, show_patch)
795 head = git.get_head()
797 patch = Patch(name, self.__patch_dir, self.__refs_dir)
801 patch.set_bottom(bottom)
803 patch.set_bottom(head)
809 patch.set_description(descr)
810 patch.set_authname(author_name)
811 patch.set_authemail(author_email)
812 patch.set_authdate(author_date)
813 patch.set_commname(committer_name)
814 patch.set_commemail(committer_email)
817 self.log_patch(patch, 'new')
819 patches = [patch.get_name()] + self.get_unapplied()
821 f = file(self.__unapplied_file, 'w+')
822 f.writelines([line + '\n' for line in patches])
824 elif before_existing:
825 self.log_patch(patch, 'new')
827 insert_string(self.__applied_file, patch.get_name())
828 if not self.get_current():
829 self.__set_current(name)
831 append_string(self.__applied_file, patch.get_name())
832 self.__set_current(name)
834 self.refresh_patch(cache_update = False, log = 'new')
836 def delete_patch(self, name):
839 self.__patch_name_valid(name)
840 patch = Patch(name, self.__patch_dir, self.__refs_dir)
842 if self.__patch_is_current(patch):
844 elif self.patch_applied(name):
845 raise StackException, 'Cannot remove an applied patch, "%s", ' \
846 'which is not current' % name
847 elif not name in self.get_unapplied():
848 raise StackException, 'Unknown patch "%s"' % name
850 # save the commit id to a trash file
851 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
855 unapplied = self.get_unapplied()
856 unapplied.remove(name)
857 f = file(self.__unapplied_file, 'w+')
858 f.writelines([line + '\n' for line in unapplied])
861 if self.patch_hidden(name):
862 self.unhide_patch(name)
864 def forward_patches(self, names):
865 """Try to fast-forward an array of patches.
867 On return, patches in names[0:returned_value] have been pushed on the
868 stack. Apply the rest with push_patch
870 unapplied = self.get_unapplied()
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 patch = Patch(name, self.__patch_dir, self.__refs_dir)
971 head = git.get_head()
972 bottom = patch.get_bottom()
973 top = patch.get_top()
978 # top != bottom always since we have a commit for each patch
980 # just make an empty patch (top = bottom = HEAD). This
981 # option is useful to allow undoing already merged
982 # patches. The top is updated by refresh_patch since we
983 # need an empty commit
984 patch.set_bottom(head, backup = True)
985 patch.set_top(head, backup = True)
988 # reset the backup information. No need for logging
989 patch.set_bottom(bottom, backup = True)
990 patch.set_top(top, backup = True)
994 # new patch needs to be refreshed.
995 # The current patch is empty after merge.
996 patch.set_bottom(head, backup = True)
997 patch.set_top(head, backup = True)
999 # Try the fast applying first. If this fails, fall back to the
1001 if not git.apply_diff(bottom, top):
1002 # if git.apply_diff() fails, the patch requires a diff3
1003 # merge and can be reported as modified
1006 # merge can fail but the patch needs to be pushed
1008 git.merge(bottom, head, top, recursive = True)
1009 except git.GitException, ex:
1010 print >> sys.stderr, \
1011 'The merge failed during "push". ' \
1012 'Use "refresh" after fixing the conflicts or ' \
1013 'revert the operation with "push --undo".'
1015 append_string(self.__applied_file, name)
1017 unapplied.remove(name)
1018 f = file(self.__unapplied_file, 'w+')
1019 f.writelines([line + '\n' for line in unapplied])
1022 self.__set_current(name)
1024 # head == bottom case doesn't need to refresh the patch
1025 if empty or head != bottom:
1027 # if the merge was OK and no conflicts, just refresh the patch
1028 # The GIT cache was already updated by the merge operation
1033 self.refresh_patch(cache_update = False, log = log)
1035 # we store the correctly merged files only for
1036 # tracking the conflict history. Note that the
1037 # git.merge() operations should always leave the index
1038 # in a valid state (i.e. only stage 0 files)
1039 self.refresh_patch(cache_update = False, log = 'push(c)')
1040 raise StackException, str(ex)
1044 def undo_push(self):
1045 name = self.get_current()
1048 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1049 old_bottom = patch.get_old_bottom()
1050 old_top = patch.get_old_top()
1052 # the top of the patch is changed by a push operation only
1053 # together with the bottom (otherwise the top was probably
1054 # modified by 'refresh'). If they are both unchanged, there
1055 # was a fast forward
1056 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1057 raise StackException, 'No undo information available'
1060 self.pop_patch(name)
1061 ret = patch.restore_old_boundaries()
1063 self.log_patch(patch, 'undo')
1067 def pop_patch(self, name, keep = False):
1068 """Pops the top patch from the stack
1070 applied = self.get_applied()
1072 assert(name in applied)
1074 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1076 # only keep the local changes
1077 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1078 raise StackException, \
1079 'Failed to pop patches while preserving the local changes'
1081 git.switch(patch.get_bottom(), keep)
1083 # save the new applied list
1084 idx = applied.index(name) + 1
1086 popped = applied[:idx]
1088 unapplied = popped + self.get_unapplied()
1090 f = file(self.__unapplied_file, 'w+')
1091 f.writelines([line + '\n' for line in unapplied])
1097 f = file(self.__applied_file, 'w+')
1098 f.writelines([line + '\n' for line in applied])
1102 self.__set_current(None)
1104 self.__set_current(applied[-1])
1106 def empty_patch(self, name):
1107 """Returns True if the patch is empty
1109 self.__patch_name_valid(name)
1110 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1111 bottom = patch.get_bottom()
1112 top = patch.get_top()
1116 elif git.get_commit(top).get_tree() \
1117 == git.get_commit(bottom).get_tree():
1122 def rename_patch(self, oldname, newname):
1123 self.__patch_name_valid(newname)
1125 applied = self.get_applied()
1126 unapplied = self.get_unapplied()
1128 if oldname == newname:
1129 raise StackException, '"To" name and "from" name are the same'
1131 if newname in applied or newname in unapplied:
1132 raise StackException, 'Patch "%s" already exists' % newname
1134 if self.patch_hidden(oldname):
1135 self.unhide_patch(oldname)
1136 self.hide_patch(newname)
1138 if oldname in unapplied:
1139 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1140 unapplied[unapplied.index(oldname)] = newname
1142 f = file(self.__unapplied_file, 'w+')
1143 f.writelines([line + '\n' for line in unapplied])
1145 elif oldname in applied:
1146 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1147 if oldname == self.get_current():
1148 self.__set_current(newname)
1150 applied[applied.index(oldname)] = newname
1152 f = file(self.__applied_file, 'w+')
1153 f.writelines([line + '\n' for line in applied])
1156 raise StackException, 'Unknown patch "%s"' % oldname
1158 def log_patch(self, patch, message):
1159 """Generate a log commit for a patch
1161 top = git.get_commit(patch.get_top())
1162 msg = '%s\t%s' % (message, top.get_id_hash())
1164 old_log = patch.get_log()
1170 log = git.commit(message = msg, parents = parents,
1171 cache_update = False, tree_id = top.get_tree(),
1175 def hide_patch(self, name):
1176 """Add the patch to the hidden list.
1178 if not self.patch_exists(name):
1179 raise StackException, 'Unknown patch "%s"' % name
1180 elif self.patch_hidden(name):
1181 raise StackException, 'Patch "%s" already hidden' % name
1183 append_string(self.__hidden_file, name)
1185 def unhide_patch(self, name):
1186 """Add the patch to the hidden list.
1188 if not self.patch_exists(name):
1189 raise StackException, 'Unknown patch "%s"' % name
1190 hidden = self.get_hidden()
1191 if not name in hidden:
1192 raise StackException, 'Patch "%s" not hidden' % name
1196 f = file(self.__hidden_file, 'w+')
1197 f.writelines([line + '\n' for line in hidden])