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
26 from shutil import copyfile
29 # stack exception class
30 class StackException(Exception):
35 self.should_print = True
36 def __call__(self, x, until_test, prefix):
38 self.should_print = False
40 return x[0:len(prefix)] != prefix
46 __comment_prefix = 'STG:'
47 __patch_prefix = 'STG_PATCH:'
49 def __clean_comments(f):
50 """Removes lines marked for status in a commit file
54 # remove status-prefixed lines
57 patch_filter = FilterUntil()
58 until_test = lambda t: t == (__patch_prefix + '\n')
59 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
61 # remove empty lines at the end
62 while len(lines) != 0 and lines[-1] == '\n':
65 f.seek(0); f.truncate()
68 def edit_file(series, line, comment, show_patch = True):
69 fname = '.stgitmsg.txt'
70 tmpl = templates.get_template('patchdescr.tmpl')
79 print >> f, __comment_prefix, comment
80 print >> f, __comment_prefix, \
81 'Lines prefixed with "%s" will be automatically removed.' \
83 print >> f, __comment_prefix, \
84 'Trailing empty lines will be automatically removed.'
87 print >> f, __patch_prefix
88 # series.get_patch(series.get_current()).get_top()
89 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
91 #Vim modeline must be near the end.
92 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
113 """An object with stgit-like properties stored as files in a directory
115 def _set_dir(self, dir):
120 def create_empty_field(self, name):
121 create_empty_file(os.path.join(self.__dir, name))
123 def _get_field(self, name, multiline = False):
124 id_file = os.path.join(self.__dir, name)
125 if os.path.isfile(id_file):
126 line = read_string(id_file, multiline)
134 def _set_field(self, name, value, multiline = False):
135 fname = os.path.join(self.__dir, name)
136 if value and value != '':
137 write_string(fname, value, multiline)
138 elif os.path.isfile(fname):
142 class Patch(StgitObject):
143 """Basic patch implementation
145 def __init__(self, name, series_dir, refs_dir):
146 self.__series_dir = series_dir
148 self._set_dir(os.path.join(self.__series_dir, self.__name))
149 self.__refs_dir = refs_dir
150 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
151 self.__log_ref_file = os.path.join(self.__refs_dir,
152 self.__name + '.log')
155 os.mkdir(self._dir())
156 self.create_empty_field('bottom')
157 self.create_empty_field('top')
160 for f in os.listdir(self._dir()):
161 os.remove(os.path.join(self._dir(), f))
162 os.rmdir(self._dir())
163 os.remove(self.__top_ref_file)
164 if os.path.exists(self.__log_ref_file):
165 os.remove(self.__log_ref_file)
170 def rename(self, newname):
172 old_top_ref_file = self.__top_ref_file
173 old_log_ref_file = self.__log_ref_file
174 self.__name = newname
175 self._set_dir(os.path.join(self.__series_dir, self.__name))
176 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
177 self.__log_ref_file = os.path.join(self.__refs_dir,
178 self.__name + '.log')
180 os.rename(olddir, self._dir())
181 os.rename(old_top_ref_file, self.__top_ref_file)
182 if os.path.exists(old_log_ref_file):
183 os.rename(old_log_ref_file, self.__log_ref_file)
185 def __update_top_ref(self, ref):
186 write_string(self.__top_ref_file, ref)
188 def __update_log_ref(self, ref):
189 write_string(self.__log_ref_file, ref)
191 def update_top_ref(self):
194 self.__update_top_ref(top)
196 def get_old_bottom(self):
197 return self._get_field('bottom.old')
199 def get_bottom(self):
200 return self._get_field('bottom')
202 def set_bottom(self, value, backup = False):
204 curr = self._get_field('bottom')
205 self._set_field('bottom.old', curr)
206 self._set_field('bottom', value)
208 def get_old_top(self):
209 return self._get_field('top.old')
212 return self._get_field('top')
214 def set_top(self, value, backup = False):
216 curr = self._get_field('top')
217 self._set_field('top.old', curr)
218 self._set_field('top', value)
219 self.__update_top_ref(value)
221 def restore_old_boundaries(self):
222 bottom = self._get_field('bottom.old')
223 top = self._get_field('top.old')
226 self._set_field('bottom', bottom)
227 self._set_field('top', top)
228 self.__update_top_ref(top)
233 def get_description(self):
234 return self._get_field('description', True)
236 def set_description(self, line):
237 self._set_field('description', line, True)
239 def get_authname(self):
240 return self._get_field('authname')
242 def set_authname(self, name):
243 self._set_field('authname', name or git.author().name)
245 def get_authemail(self):
246 return self._get_field('authemail')
248 def set_authemail(self, email):
249 self._set_field('authemail', email or git.author().email)
251 def get_authdate(self):
252 return self._get_field('authdate')
254 def set_authdate(self, date):
255 self._set_field('authdate', date or git.author().date)
257 def get_commname(self):
258 return self._get_field('commname')
260 def set_commname(self, name):
261 self._set_field('commname', name or git.committer().name)
263 def get_commemail(self):
264 return self._get_field('commemail')
266 def set_commemail(self, email):
267 self._set_field('commemail', email or git.committer().email)
270 return self._get_field('log')
272 def set_log(self, value, backup = False):
273 self._set_field('log', value)
274 self.__update_log_ref(value)
277 class Series(StgitObject):
278 """Class including the operations on series
280 def __init__(self, name = None):
281 """Takes a series name as the parameter.
287 self.__name = git.get_head_file()
288 self.__base_dir = basedir.get()
289 except git.GitException, ex:
290 raise StackException, 'GIT tree not initialised: %s' % ex
292 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
293 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
296 self.__applied_file = os.path.join(self._dir(), 'applied')
297 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
298 self.__hidden_file = os.path.join(self._dir(), 'hidden')
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 get_patch(self, name):
329 """Return a Patch object for the given name
331 return Patch(name, self.__patch_dir, self.__refs_dir)
333 def get_current_patch(self):
334 """Return a Patch object representing the topmost patch, or
335 None if there is no such patch."""
336 crt = self.get_current()
339 return Patch(crt, self.__patch_dir, self.__refs_dir)
341 def get_current(self):
342 """Return the name of the topmost patch, or None if there is
345 applied = self.get_applied()
346 except StackException:
347 # No "applied" file: branch is not initialized.
352 # No patches applied.
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):
454 self.__set_parent_remote(remote)
455 self.__set_parent_branch(localbranch)
456 # We'll enforce this later
458 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
460 def __patch_is_current(self, patch):
461 return patch.get_name() == self.get_current()
463 def patch_applied(self, name):
464 """Return true if the patch exists in the applied list
466 return name in self.get_applied()
468 def patch_unapplied(self, name):
469 """Return true if the patch exists in the unapplied list
471 return name in self.get_unapplied()
473 def patch_hidden(self, name):
474 """Return true if the patch is hidden.
476 return name in self.get_hidden()
478 def patch_exists(self, name):
479 """Return true if there is a patch with the given name, false
481 return self.patch_applied(name) or self.patch_unapplied(name)
483 def head_top_equal(self):
484 """Return true if the head and the top are the same
486 crt = self.get_current_patch()
488 # we don't care, no patches applied
490 return git.get_head() == crt.get_top()
492 def is_initialised(self):
493 """Checks if series is already initialised
495 return os.path.isdir(self.__patch_dir)
497 def init(self, create_at=False, parent_remote=None, parent_branch=None):
498 """Initialises the stgit series
500 if os.path.exists(self.__patch_dir):
501 raise StackException, self.__patch_dir + ' already exists'
502 if os.path.exists(self.__refs_dir):
503 raise StackException, self.__refs_dir + ' already exists'
505 if (create_at!=False):
506 git.create_branch(self.__name, create_at)
508 os.makedirs(self.__patch_dir)
510 self.set_parent(parent_remote, parent_branch)
512 self.create_empty_field('applied')
513 self.create_empty_field('unapplied')
514 self.create_empty_field('description')
515 os.makedirs(os.path.join(self._dir(), 'patches'))
516 os.makedirs(self.__refs_dir)
517 self._set_field('orig-base', git.get_head())
520 """Either convert to use a separate patch directory, or
521 unconvert to place the patches in the same directory with
524 if self.__patch_dir == self._dir():
525 print 'Converting old-style to new-style...',
528 self.__patch_dir = os.path.join(self._dir(), 'patches')
529 os.makedirs(self.__patch_dir)
531 for p in self.get_applied() + self.get_unapplied():
532 src = os.path.join(self._dir(), p)
533 dest = os.path.join(self.__patch_dir, p)
539 print 'Converting new-style to old-style...',
542 for p in self.get_applied() + self.get_unapplied():
543 src = os.path.join(self.__patch_dir, p)
544 dest = os.path.join(self._dir(), p)
547 if not os.listdir(self.__patch_dir):
548 os.rmdir(self.__patch_dir)
551 print 'Patch directory %s is not empty.' % self.__patch_dir
553 self.__patch_dir = self._dir()
555 def rename(self, to_name):
558 to_stack = Series(to_name)
560 if to_stack.is_initialised():
561 raise StackException, '"%s" already exists' % to_stack.get_branch()
563 git.rename_branch(self.__name, to_name)
565 if os.path.isdir(self._dir()):
566 rename(os.path.join(self.__base_dir, 'patches'),
567 self.__name, to_stack.__name)
568 if os.path.exists(self.__refs_dir):
569 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
570 self.__name, to_stack.__name)
572 # Rename the config section
573 config.rename_section("branch.%s" % self.__name,
574 "branch.%s" % to_name)
576 self.__init__(to_name)
578 def clone(self, target_series):
582 # allow cloning of branches not under StGIT control
583 base = self.get_base()
585 base = git.get_head()
586 Series(target_series).init(create_at = base)
587 new_series = Series(target_series)
589 # generate an artificial description file
590 new_series.set_description('clone of "%s"' % self.__name)
592 # clone self's entire series as unapplied patches
594 # allow cloning of branches not under StGIT control
595 applied = self.get_applied()
596 unapplied = self.get_unapplied()
597 patches = applied + unapplied
600 patches = applied = unapplied = []
602 patch = self.get_patch(p)
603 newpatch = new_series.new_patch(p, message = patch.get_description(),
604 can_edit = False, unapplied = True,
605 bottom = patch.get_bottom(),
606 top = patch.get_top(),
607 author_name = patch.get_authname(),
608 author_email = patch.get_authemail(),
609 author_date = patch.get_authdate())
611 print "setting log to %s" % patch.get_log()
612 newpatch.set_log(patch.get_log())
614 print "no log for %s" % p
616 # fast forward the cloned series to self's top
617 new_series.forward_patches(applied)
619 # Clone parent informations
620 value = config.get('branch.%s.remote' % self.__name)
622 config.set('branch.%s.remote' % target_series, value)
624 value = config.get('branch.%s.merge' % self.__name)
626 config.set('branch.%s.merge' % target_series, value)
628 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
630 config.set('branch.%s.stgit.parentbranch' % target_series, value)
632 def delete(self, force = False):
633 """Deletes an stgit series
635 if self.is_initialised():
636 patches = self.get_unapplied() + self.get_applied()
637 if not force and patches:
638 raise StackException, \
639 'Cannot delete: the series still contains patches'
641 Patch(p, self.__patch_dir, self.__refs_dir).delete()
643 # remove the trash directory
644 for fname in os.listdir(self.__trash_dir):
645 os.remove(os.path.join(self.__trash_dir, fname))
646 os.rmdir(self.__trash_dir)
648 # FIXME: find a way to get rid of those manual removals
649 # (move functionality to StgitObject ?)
650 if os.path.exists(self.__applied_file):
651 os.remove(self.__applied_file)
652 if os.path.exists(self.__unapplied_file):
653 os.remove(self.__unapplied_file)
654 if os.path.exists(self.__hidden_file):
655 os.remove(self.__hidden_file)
656 if os.path.exists(self.__descr_file):
657 os.remove(self.__descr_file)
658 if os.path.exists(self._dir()+'/orig-base'):
659 os.remove(self._dir()+'/orig-base')
661 if not os.listdir(self.__patch_dir):
662 os.rmdir(self.__patch_dir)
664 print 'Patch directory %s is not empty.' % self.__patch_dir
667 os.removedirs(self._dir())
669 raise StackException, 'Series directory %s is not empty.' % self._dir()
672 os.removedirs(self.__refs_dir)
674 print 'Refs directory %s is not empty.' % self.__refs_dir
676 # Cleanup parent informations
677 # FIXME: should one day make use of git-config --section-remove,
678 # scheduled for 1.5.1
679 config.unset('branch.%s.remote' % self.__name)
680 config.unset('branch.%s.merge' % self.__name)
681 config.unset('branch.%s.stgit.parentbranch' % self.__name)
683 def refresh_patch(self, files = None, message = None, edit = False,
686 author_name = None, author_email = None,
688 committer_name = None, committer_email = None,
689 backup = False, sign_str = None, log = 'refresh'):
690 """Generates a new commit for the given patch
692 name = self.get_current()
694 raise StackException, 'No patches applied'
696 patch = Patch(name, self.__patch_dir, self.__refs_dir)
698 descr = patch.get_description()
699 if not (message or descr):
705 if not message and edit:
706 descr = edit_file(self, descr.rstrip(), \
707 'Please edit the description for patch "%s" ' \
708 'above.' % name, show_patch)
711 author_name = patch.get_authname()
713 author_email = patch.get_authemail()
715 author_date = patch.get_authdate()
716 if not committer_name:
717 committer_name = patch.get_commname()
718 if not committer_email:
719 committer_email = patch.get_commemail()
722 descr = descr.rstrip()
723 if descr.find("\nSigned-off-by:") < 0 \
724 and descr.find("\nAcked-by:") < 0:
727 descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
728 committer_name, committer_email)
730 bottom = patch.get_bottom()
732 commit_id = git.commit(files = files,
733 message = descr, parents = [bottom],
734 cache_update = cache_update,
736 author_name = author_name,
737 author_email = author_email,
738 author_date = author_date,
739 committer_name = committer_name,
740 committer_email = committer_email)
742 patch.set_bottom(bottom, backup = backup)
743 patch.set_top(commit_id, backup = backup)
744 patch.set_description(descr)
745 patch.set_authname(author_name)
746 patch.set_authemail(author_email)
747 patch.set_authdate(author_date)
748 patch.set_commname(committer_name)
749 patch.set_commemail(committer_email)
752 self.log_patch(patch, log)
756 def undo_refresh(self):
757 """Undo the patch boundaries changes caused by 'refresh'
759 name = self.get_current()
762 patch = Patch(name, self.__patch_dir, self.__refs_dir)
763 old_bottom = patch.get_old_bottom()
764 old_top = patch.get_old_top()
766 # the bottom of the patch is not changed by refresh. If the
767 # old_bottom is different, there wasn't any previous 'refresh'
768 # command (probably only a 'push')
769 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
770 raise StackException, 'No undo information available'
772 git.reset(tree_id = old_top, check_out = False)
773 if patch.restore_old_boundaries():
774 self.log_patch(patch, 'undo')
776 def new_patch(self, name, message = None, can_edit = True,
777 unapplied = False, show_patch = False,
778 top = None, bottom = None,
779 author_name = None, author_email = None, author_date = None,
780 committer_name = None, committer_email = None,
781 before_existing = False, refresh = True):
782 """Creates a new patch
784 self.__patch_name_valid(name)
786 if self.patch_applied(name) or self.patch_unapplied(name):
787 raise StackException, 'Patch "%s" already exists' % name
789 if not message and can_edit:
790 descr = edit_file(self, None, \
791 'Please enter the description for patch "%s" ' \
792 'above.' % name, show_patch)
796 head = git.get_head()
798 patch = Patch(name, self.__patch_dir, self.__refs_dir)
802 patch.set_bottom(bottom)
804 patch.set_bottom(head)
810 patch.set_description(descr)
811 patch.set_authname(author_name)
812 patch.set_authemail(author_email)
813 patch.set_authdate(author_date)
814 patch.set_commname(committer_name)
815 patch.set_commemail(committer_email)
818 self.log_patch(patch, 'new')
820 patches = [patch.get_name()] + self.get_unapplied()
822 f = file(self.__unapplied_file, 'w+')
823 f.writelines([line + '\n' for line in patches])
825 elif before_existing:
826 self.log_patch(patch, 'new')
828 insert_string(self.__applied_file, patch.get_name())
830 append_string(self.__applied_file, patch.get_name())
832 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])
941 def merged_patches(self, names):
942 """Test which patches were merged upstream by reverse-applying
943 them in reverse order. The function returns the list of
944 patches detected to have been applied. The state of the tree
945 is restored to the original one
947 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
953 if git.apply_diff(p.get_top(), p.get_bottom()):
954 merged.append(p.get_name())
961 def push_patch(self, name, empty = False):
962 """Pushes a patch on the stack
964 unapplied = self.get_unapplied()
965 assert(name in unapplied)
967 patch = Patch(name, self.__patch_dir, self.__refs_dir)
969 head = git.get_head()
970 bottom = patch.get_bottom()
971 top = patch.get_top()
976 # top != bottom always since we have a commit for each patch
978 # just make an empty patch (top = bottom = HEAD). This
979 # option is useful to allow undoing already merged
980 # patches. The top is updated by refresh_patch since we
981 # need an empty commit
982 patch.set_bottom(head, backup = True)
983 patch.set_top(head, backup = True)
986 # reset the backup information. No need for logging
987 patch.set_bottom(bottom, backup = True)
988 patch.set_top(top, backup = True)
992 # new patch needs to be refreshed.
993 # The current patch is empty after merge.
994 patch.set_bottom(head, backup = True)
995 patch.set_top(head, backup = True)
997 # Try the fast applying first. If this fails, fall back to the
999 if not git.apply_diff(bottom, top):
1000 # if git.apply_diff() fails, the patch requires a diff3
1001 # merge and can be reported as modified
1004 # merge can fail but the patch needs to be pushed
1006 git.merge(bottom, head, top, recursive = True)
1007 except git.GitException, ex:
1008 print >> sys.stderr, \
1009 'The merge failed during "push". ' \
1010 'Use "refresh" after fixing the conflicts or ' \
1011 'revert the operation with "push --undo".'
1013 append_string(self.__applied_file, name)
1015 unapplied.remove(name)
1016 f = file(self.__unapplied_file, 'w+')
1017 f.writelines([line + '\n' for line in unapplied])
1020 # head == bottom case doesn't need to refresh the patch
1021 if empty or head != bottom:
1023 # if the merge was OK and no conflicts, just refresh the patch
1024 # The GIT cache was already updated by the merge operation
1029 self.refresh_patch(cache_update = False, log = log)
1031 # we store the correctly merged files only for
1032 # tracking the conflict history. Note that the
1033 # git.merge() operations should always leave the index
1034 # in a valid state (i.e. only stage 0 files)
1035 self.refresh_patch(cache_update = False, log = 'push(c)')
1036 raise StackException, str(ex)
1040 def undo_push(self):
1041 name = self.get_current()
1044 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1045 old_bottom = patch.get_old_bottom()
1046 old_top = patch.get_old_top()
1048 # the top of the patch is changed by a push operation only
1049 # together with the bottom (otherwise the top was probably
1050 # modified by 'refresh'). If they are both unchanged, there
1051 # was a fast forward
1052 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1053 raise StackException, 'No undo information available'
1056 self.pop_patch(name)
1057 ret = patch.restore_old_boundaries()
1059 self.log_patch(patch, 'undo')
1063 def pop_patch(self, name, keep = False):
1064 """Pops the top patch from the stack
1066 applied = self.get_applied()
1068 assert(name in applied)
1070 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1072 if git.get_head_file() == self.get_branch():
1073 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1074 raise StackException(
1075 'Failed to pop patches while preserving the local changes')
1076 git.switch(patch.get_bottom(), keep)
1078 git.set_branch(self.get_branch(), patch.get_bottom())
1080 # save the new applied list
1081 idx = applied.index(name) + 1
1083 popped = applied[:idx]
1085 unapplied = popped + self.get_unapplied()
1087 f = file(self.__unapplied_file, 'w+')
1088 f.writelines([line + '\n' for line in unapplied])
1094 f = file(self.__applied_file, 'w+')
1095 f.writelines([line + '\n' for line in applied])
1098 def empty_patch(self, name):
1099 """Returns True if the patch is empty
1101 self.__patch_name_valid(name)
1102 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1103 bottom = patch.get_bottom()
1104 top = patch.get_top()
1108 elif git.get_commit(top).get_tree() \
1109 == git.get_commit(bottom).get_tree():
1114 def rename_patch(self, oldname, newname):
1115 self.__patch_name_valid(newname)
1117 applied = self.get_applied()
1118 unapplied = self.get_unapplied()
1120 if oldname == newname:
1121 raise StackException, '"To" name and "from" name are the same'
1123 if newname in applied or newname in unapplied:
1124 raise StackException, 'Patch "%s" already exists' % newname
1126 if self.patch_hidden(oldname):
1127 self.unhide_patch(oldname)
1128 self.hide_patch(newname)
1130 if oldname in unapplied:
1131 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1132 unapplied[unapplied.index(oldname)] = newname
1134 f = file(self.__unapplied_file, 'w+')
1135 f.writelines([line + '\n' for line in unapplied])
1137 elif oldname in applied:
1138 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1140 applied[applied.index(oldname)] = newname
1142 f = file(self.__applied_file, 'w+')
1143 f.writelines([line + '\n' for line in applied])
1146 raise StackException, 'Unknown patch "%s"' % oldname
1148 def log_patch(self, patch, message):
1149 """Generate a log commit for a patch
1151 top = git.get_commit(patch.get_top())
1152 msg = '%s\t%s' % (message, top.get_id_hash())
1154 old_log = patch.get_log()
1160 log = git.commit(message = msg, parents = parents,
1161 cache_update = False, tree_id = top.get_tree(),
1165 def hide_patch(self, name):
1166 """Add the patch to the hidden list.
1168 if not self.patch_exists(name):
1169 raise StackException, 'Unknown patch "%s"' % name
1170 elif self.patch_hidden(name):
1171 raise StackException, 'Patch "%s" already hidden' % name
1173 append_string(self.__hidden_file, name)
1175 def unhide_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 hidden = self.get_hidden()
1181 if not name in hidden:
1182 raise StackException, 'Patch "%s" not hidden' % name
1186 f = file(self.__hidden_file, 'w+')
1187 f.writelines([line + '\n' for line in hidden])