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
786 self.__patch_name_valid(name)
787 if self.patch_applied(name) or self.patch_unapplied(name):
788 raise StackException, 'Patch "%s" already exists' % name
790 if not message and can_edit:
793 'Please enter the description for the patch above.',
798 head = git.get_head()
801 name = make_patch_name(descr, self.patch_exists)
803 patch = Patch(name, self.__patch_dir, self.__refs_dir)
807 patch.set_bottom(bottom)
809 patch.set_bottom(head)
815 patch.set_description(descr)
816 patch.set_authname(author_name)
817 patch.set_authemail(author_email)
818 patch.set_authdate(author_date)
819 patch.set_commname(committer_name)
820 patch.set_commemail(committer_email)
823 self.log_patch(patch, 'new')
825 patches = [patch.get_name()] + self.get_unapplied()
827 f = file(self.__unapplied_file, 'w+')
828 f.writelines([line + '\n' for line in patches])
830 elif before_existing:
831 self.log_patch(patch, 'new')
833 insert_string(self.__applied_file, patch.get_name())
835 append_string(self.__applied_file, patch.get_name())
837 self.refresh_patch(cache_update = False, log = 'new')
841 def delete_patch(self, name):
844 self.__patch_name_valid(name)
845 patch = Patch(name, self.__patch_dir, self.__refs_dir)
847 if self.__patch_is_current(patch):
849 elif self.patch_applied(name):
850 raise StackException, 'Cannot remove an applied patch, "%s", ' \
851 'which is not current' % name
852 elif not name in self.get_unapplied():
853 raise StackException, 'Unknown patch "%s"' % name
855 # save the commit id to a trash file
856 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
860 unapplied = self.get_unapplied()
861 unapplied.remove(name)
862 f = file(self.__unapplied_file, 'w+')
863 f.writelines([line + '\n' for line in unapplied])
866 if self.patch_hidden(name):
867 self.unhide_patch(name)
869 def forward_patches(self, names):
870 """Try to fast-forward an array of patches.
872 On return, patches in names[0:returned_value] have been pushed on the
873 stack. Apply the rest with push_patch
875 unapplied = self.get_unapplied()
881 assert(name in unapplied)
883 patch = Patch(name, self.__patch_dir, self.__refs_dir)
886 bottom = patch.get_bottom()
887 top = patch.get_top()
889 # top != bottom always since we have a commit for each patch
891 # reset the backup information. No logging since the
892 # patch hasn't changed
893 patch.set_bottom(head, backup = True)
894 patch.set_top(top, backup = True)
897 head_tree = git.get_commit(head).get_tree()
898 bottom_tree = git.get_commit(bottom).get_tree()
899 if head_tree == bottom_tree:
900 # We must just reparent this patch and create a new commit
902 descr = patch.get_description()
903 author_name = patch.get_authname()
904 author_email = patch.get_authemail()
905 author_date = patch.get_authdate()
906 committer_name = patch.get_commname()
907 committer_email = patch.get_commemail()
909 top_tree = git.get_commit(top).get_tree()
911 top = git.commit(message = descr, parents = [head],
912 cache_update = False,
915 author_name = author_name,
916 author_email = author_email,
917 author_date = author_date,
918 committer_name = committer_name,
919 committer_email = committer_email)
921 patch.set_bottom(head, backup = True)
922 patch.set_top(top, backup = True)
924 self.log_patch(patch, 'push(f)')
927 # stop the fast-forwarding, must do a real merge
931 unapplied.remove(name)
938 append_strings(self.__applied_file, names[0:forwarded])
940 f = file(self.__unapplied_file, 'w+')
941 f.writelines([line + '\n' for line in unapplied])
946 def merged_patches(self, names):
947 """Test which patches were merged upstream by reverse-applying
948 them in reverse order. The function returns the list of
949 patches detected to have been applied. The state of the tree
950 is restored to the original one
952 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
958 if git.apply_diff(p.get_top(), p.get_bottom()):
959 merged.append(p.get_name())
966 def push_patch(self, name, empty = False):
967 """Pushes a patch on the stack
969 unapplied = self.get_unapplied()
970 assert(name in unapplied)
972 patch = Patch(name, self.__patch_dir, self.__refs_dir)
974 head = git.get_head()
975 bottom = patch.get_bottom()
976 top = patch.get_top()
981 # top != bottom always since we have a commit for each patch
983 # just make an empty patch (top = bottom = HEAD). This
984 # option is useful to allow undoing already merged
985 # patches. The top is updated by refresh_patch since we
986 # need an empty commit
987 patch.set_bottom(head, backup = True)
988 patch.set_top(head, backup = True)
991 # reset the backup information. No need for logging
992 patch.set_bottom(bottom, backup = True)
993 patch.set_top(top, backup = True)
997 # new patch needs to be refreshed.
998 # The current patch is empty after merge.
999 patch.set_bottom(head, backup = True)
1000 patch.set_top(head, backup = True)
1002 # Try the fast applying first. If this fails, fall back to the
1004 if not git.apply_diff(bottom, top):
1005 # if git.apply_diff() fails, the patch requires a diff3
1006 # merge and can be reported as modified
1009 # merge can fail but the patch needs to be pushed
1011 git.merge(bottom, head, top, recursive = True)
1012 except git.GitException, ex:
1013 print >> sys.stderr, \
1014 'The merge failed during "push". ' \
1015 'Use "refresh" after fixing the conflicts or ' \
1016 'revert the operation with "push --undo".'
1018 append_string(self.__applied_file, name)
1020 unapplied.remove(name)
1021 f = file(self.__unapplied_file, 'w+')
1022 f.writelines([line + '\n' for line in unapplied])
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 should 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 if git.get_head_file() == self.get_branch():
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')
1081 git.switch(patch.get_bottom(), keep)
1083 git.set_branch(self.get_branch(), patch.get_bottom())
1085 # save the new applied list
1086 idx = applied.index(name) + 1
1088 popped = applied[:idx]
1090 unapplied = popped + self.get_unapplied()
1092 f = file(self.__unapplied_file, 'w+')
1093 f.writelines([line + '\n' for line in unapplied])
1099 f = file(self.__applied_file, 'w+')
1100 f.writelines([line + '\n' for line in applied])
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)
1145 applied[applied.index(oldname)] = newname
1147 f = file(self.__applied_file, 'w+')
1148 f.writelines([line + '\n' for line in applied])
1151 raise StackException, 'Unknown patch "%s"' % oldname
1153 def log_patch(self, patch, message):
1154 """Generate a log commit for a patch
1156 top = git.get_commit(patch.get_top())
1157 msg = '%s\t%s' % (message, top.get_id_hash())
1159 old_log = patch.get_log()
1165 log = git.commit(message = msg, parents = parents,
1166 cache_update = False, tree_id = top.get_tree(),
1170 def hide_patch(self, name):
1171 """Add the patch to the hidden list.
1173 if not self.patch_exists(name):
1174 raise StackException, 'Unknown patch "%s"' % name
1175 elif self.patch_hidden(name):
1176 raise StackException, 'Patch "%s" already hidden' % name
1178 append_string(self.__hidden_file, name)
1180 def unhide_patch(self, name):
1181 """Add the patch to the hidden list.
1183 if not self.patch_exists(name):
1184 raise StackException, 'Unknown patch "%s"' % name
1185 hidden = self.get_hidden()
1186 if not name in hidden:
1187 raise StackException, 'Patch "%s" not hidden' % name
1191 f = file(self.__hidden_file, 'w+')
1192 f.writelines([line + '\n' for line in hidden])