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.__current_file = os.path.join(self._dir(), 'current')
300 self.__descr_file = os.path.join(self._dir(), 'description')
302 # where this series keeps its patches
303 self.__patch_dir = os.path.join(self._dir(), 'patches')
304 if not os.path.isdir(self.__patch_dir):
305 self.__patch_dir = self._dir()
307 # if no __refs_dir, create and populate it (upgrade old repositories)
308 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
309 os.makedirs(self.__refs_dir)
310 for patch in self.get_applied() + self.get_unapplied():
311 self.get_patch(patch).update_top_ref()
314 self.__trash_dir = os.path.join(self._dir(), 'trash')
315 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
316 os.makedirs(self.__trash_dir)
318 def __patch_name_valid(self, name):
319 """Raise an exception if the patch name is not valid.
321 if not name or re.search('[^\w.-]', name):
322 raise StackException, 'Invalid patch name: "%s"' % name
324 def get_branch(self):
325 """Return the branch name for the Series object
329 def __set_current(self, name):
330 """Sets the topmost patch
332 self._set_field('current', name)
334 def get_patch(self, name):
335 """Return a Patch object for the given name
337 return Patch(name, self.__patch_dir, self.__refs_dir)
339 def get_current_patch(self):
340 """Return a Patch object representing the topmost patch, or
341 None if there is no such patch."""
342 crt = self.get_current()
345 return Patch(crt, self.__patch_dir, self.__refs_dir)
347 def get_current(self):
348 """Return the name of the topmost patch, or None if there is
350 name = self._get_field('current')
356 def get_applied(self):
357 if not os.path.isfile(self.__applied_file):
358 raise StackException, 'Branch "%s" not initialised' % self.__name
359 f = file(self.__applied_file)
360 names = [line.strip() for line in f.readlines()]
364 def get_unapplied(self):
365 if not os.path.isfile(self.__unapplied_file):
366 raise StackException, 'Branch "%s" not initialised' % self.__name
367 f = file(self.__unapplied_file)
368 names = [line.strip() for line in f.readlines()]
372 def get_hidden(self):
373 if not os.path.isfile(self.__hidden_file):
375 f = file(self.__hidden_file)
376 names = [line.strip() for line in f.readlines()]
381 # Return the parent of the bottommost patch, if there is one.
382 if os.path.isfile(self.__applied_file):
383 bottommost = file(self.__applied_file).readline().strip()
385 return self.get_patch(bottommost).get_bottom()
386 # No bottommost patch, so just return HEAD
387 return git.get_head()
390 """Return the head of the branch
392 crt = self.get_current_patch()
396 return self.get_base()
398 def get_protected(self):
399 return os.path.isfile(os.path.join(self._dir(), 'protected'))
402 protect_file = os.path.join(self._dir(), 'protected')
403 if not os.path.isfile(protect_file):
404 create_empty_file(protect_file)
407 protect_file = os.path.join(self._dir(), 'protected')
408 if os.path.isfile(protect_file):
409 os.remove(protect_file)
411 def get_description(self):
412 return self._get_field('description') or ''
414 def set_description(self, line):
415 self._set_field('description', line)
417 def get_parent_remote(self):
418 value = config.get('branch.%s.remote' % self.__name)
421 elif 'origin' in git.remotes_list():
422 print 'Notice: no parent remote declared for stack "%s", ' \
423 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
424 'and "branch.%s.merge" with "git repo-config".' \
425 % (self.__name, self.__name, self.__name)
428 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
430 def __set_parent_remote(self, remote):
431 value = config.set('branch.%s.remote' % self.__name, remote)
433 def get_parent_branch(self):
434 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
437 elif git.rev_parse('heads/origin'):
438 print 'Notice: no parent branch declared for stack "%s", ' \
439 'defaulting to "heads/origin". Consider setting ' \
440 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
441 % (self.__name, self.__name)
442 return 'heads/origin'
444 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
446 def __set_parent_branch(self, name):
447 if config.get('branch.%s.remote' % self.__name):
448 # Never set merge if remote is not set to avoid
449 # possibly-erroneous lookups into 'origin'
450 config.set('branch.%s.merge' % self.__name, name)
451 config.set('branch.%s.stgit.parentbranch' % self.__name, name)
453 def set_parent(self, remote, localbranch):
454 # policy: record local branches as remote='.'
455 recordremote = remote or '.'
457 self.__set_parent_remote(recordremote)
458 self.__set_parent_branch(localbranch)
459 # We'll enforce this later
461 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
463 def __patch_is_current(self, patch):
464 return patch.get_name() == self.get_current()
466 def patch_applied(self, name):
467 """Return true if the patch exists in the applied list
469 return name in self.get_applied()
471 def patch_unapplied(self, name):
472 """Return true if the patch exists in the unapplied list
474 return name in self.get_unapplied()
476 def patch_hidden(self, name):
477 """Return true if the patch is hidden.
479 return name in self.get_hidden()
481 def patch_exists(self, name):
482 """Return true if there is a patch with the given name, false
484 return self.patch_applied(name) or self.patch_unapplied(name)
486 def head_top_equal(self):
487 """Return true if the head and the top are the same
489 crt = self.get_current_patch()
491 # we don't care, no patches applied
493 return git.get_head() == crt.get_top()
495 def is_initialised(self):
496 """Checks if series is already initialised
498 return os.path.isdir(self.__patch_dir)
500 def init(self, create_at=False, parent_remote=None, parent_branch=None):
501 """Initialises the stgit series
503 if os.path.exists(self.__patch_dir):
504 raise StackException, self.__patch_dir + ' already exists'
505 if os.path.exists(self.__refs_dir):
506 raise StackException, self.__refs_dir + ' already exists'
508 if (create_at!=False):
509 git.create_branch(self.__name, create_at)
511 os.makedirs(self.__patch_dir)
513 self.set_parent(parent_remote, parent_branch)
515 self.create_empty_field('applied')
516 self.create_empty_field('unapplied')
517 self.create_empty_field('description')
518 os.makedirs(os.path.join(self._dir(), 'patches'))
519 os.makedirs(self.__refs_dir)
520 self._set_field('orig-base', git.get_head())
523 """Either convert to use a separate patch directory, or
524 unconvert to place the patches in the same directory with
527 if self.__patch_dir == self._dir():
528 print 'Converting old-style to new-style...',
531 self.__patch_dir = os.path.join(self._dir(), 'patches')
532 os.makedirs(self.__patch_dir)
534 for p in self.get_applied() + self.get_unapplied():
535 src = os.path.join(self._dir(), p)
536 dest = os.path.join(self.__patch_dir, p)
542 print 'Converting new-style to old-style...',
545 for p in self.get_applied() + self.get_unapplied():
546 src = os.path.join(self.__patch_dir, p)
547 dest = os.path.join(self._dir(), p)
550 if not os.listdir(self.__patch_dir):
551 os.rmdir(self.__patch_dir)
554 print 'Patch directory %s is not empty.' % self.__patch_dir
556 self.__patch_dir = self._dir()
558 def rename(self, to_name):
561 to_stack = Series(to_name)
563 if to_stack.is_initialised():
564 raise StackException, '"%s" already exists' % to_stack.get_branch()
566 git.rename_branch(self.__name, to_name)
568 if os.path.isdir(self._dir()):
569 rename(os.path.join(self.__base_dir, 'patches'),
570 self.__name, to_stack.__name)
571 if os.path.exists(self.__refs_dir):
572 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
573 self.__name, to_stack.__name)
575 # Rename the config section
576 config.rename_section("branch.%s" % self.__name,
577 "branch.%s" % to_name)
579 self.__init__(to_name)
581 def clone(self, target_series):
585 # allow cloning of branches not under StGIT control
586 base = self.get_base()
588 base = git.get_head()
589 Series(target_series).init(create_at = base)
590 new_series = Series(target_series)
592 # generate an artificial description file
593 new_series.set_description('clone of "%s"' % self.__name)
595 # clone self's entire series as unapplied patches
597 # allow cloning of branches not under StGIT control
598 applied = self.get_applied()
599 unapplied = self.get_unapplied()
600 patches = applied + unapplied
603 patches = applied = unapplied = []
605 patch = self.get_patch(p)
606 newpatch = new_series.new_patch(p, message = patch.get_description(),
607 can_edit = False, unapplied = True,
608 bottom = patch.get_bottom(),
609 top = patch.get_top(),
610 author_name = patch.get_authname(),
611 author_email = patch.get_authemail(),
612 author_date = patch.get_authdate())
614 print "setting log to %s" % patch.get_log()
615 newpatch.set_log(patch.get_log())
617 print "no log for %s" % patchname
619 # fast forward the cloned series to self's top
620 new_series.forward_patches(applied)
622 # Clone parent informations
623 value = config.get('branch.%s.remote' % self.__name)
625 config.set('branch.%s.remote' % target_series, value)
627 value = config.get('branch.%s.merge' % self.__name)
629 config.set('branch.%s.merge' % target_series, value)
631 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
633 config.set('branch.%s.stgit.parentbranch' % target_series, value)
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):
648 os.remove(os.path.join(self.__trash_dir, fname))
649 os.rmdir(self.__trash_dir)
651 # FIXME: find a way to get rid of those manual removals
652 # (move functionality 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 os.path.exists(self._dir()+'/orig-base'):
664 os.remove(self._dir()+'/orig-base')
666 if not os.listdir(self.__patch_dir):
667 os.rmdir(self.__patch_dir)
669 print 'Patch directory %s is not empty.' % self.__patch_dir
672 os.removedirs(self._dir())
674 raise StackException, 'Series directory %s is not empty.' % self._dir()
677 os.removedirs(self.__refs_dir)
679 print 'Refs directory %s is not empty.' % self.__refs_dir
681 # Cleanup parent informations
682 # FIXME: should one day make use of git-config --section-remove,
683 # scheduled for 1.5.1
684 config.unset('branch.%s.remote' % self.__name)
685 config.unset('branch.%s.merge' % self.__name)
686 config.unset('branch.%s.stgit.parentbranch' % self.__name)
688 def refresh_patch(self, files = None, message = None, edit = False,
691 author_name = None, author_email = None,
693 committer_name = None, committer_email = None,
694 backup = False, sign_str = None, log = 'refresh'):
695 """Generates a new commit for the given patch
697 name = self.get_current()
699 raise StackException, 'No patches applied'
701 patch = Patch(name, self.__patch_dir, self.__refs_dir)
703 descr = patch.get_description()
704 if not (message or descr):
710 if not message and edit:
711 descr = edit_file(self, descr.rstrip(), \
712 'Please edit the description for patch "%s" ' \
713 'above.' % name, show_patch)
716 author_name = patch.get_authname()
718 author_email = patch.get_authemail()
720 author_date = patch.get_authdate()
721 if not committer_name:
722 committer_name = patch.get_commname()
723 if not committer_email:
724 committer_email = patch.get_commemail()
727 descr = descr.rstrip()
728 if descr.find("\nSigned-off-by:") < 0 \
729 and descr.find("\nAcked-by:") < 0:
732 descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
733 committer_name, committer_email)
735 bottom = patch.get_bottom()
737 commit_id = git.commit(files = files,
738 message = descr, parents = [bottom],
739 cache_update = cache_update,
741 author_name = author_name,
742 author_email = author_email,
743 author_date = author_date,
744 committer_name = committer_name,
745 committer_email = committer_email)
747 patch.set_bottom(bottom, backup = backup)
748 patch.set_top(commit_id, backup = backup)
749 patch.set_description(descr)
750 patch.set_authname(author_name)
751 patch.set_authemail(author_email)
752 patch.set_authdate(author_date)
753 patch.set_commname(committer_name)
754 patch.set_commemail(committer_email)
757 self.log_patch(patch, log)
761 def undo_refresh(self):
762 """Undo the patch boundaries changes caused by 'refresh'
764 name = self.get_current()
767 patch = Patch(name, self.__patch_dir, self.__refs_dir)
768 old_bottom = patch.get_old_bottom()
769 old_top = patch.get_old_top()
771 # the bottom of the patch is not changed by refresh. If the
772 # old_bottom is different, there wasn't any previous 'refresh'
773 # command (probably only a 'push')
774 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
775 raise StackException, 'No undo information available'
777 git.reset(tree_id = old_top, check_out = False)
778 if patch.restore_old_boundaries():
779 self.log_patch(patch, 'undo')
781 def new_patch(self, name, message = None, can_edit = True,
782 unapplied = False, show_patch = False,
783 top = None, bottom = None,
784 author_name = None, author_email = None, author_date = None,
785 committer_name = None, committer_email = None,
786 before_existing = False, refresh = True):
787 """Creates a new patch
789 self.__patch_name_valid(name)
791 if self.patch_applied(name) or self.patch_unapplied(name):
792 raise StackException, 'Patch "%s" already exists' % name
794 if not message and can_edit:
795 descr = edit_file(self, None, \
796 'Please enter the description for patch "%s" ' \
797 'above.' % name, show_patch)
801 head = git.get_head()
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())
834 if not self.get_current():
835 self.__set_current(name)
837 append_string(self.__applied_file, patch.get_name())
838 self.__set_current(name)
840 self.refresh_patch(cache_update = False, log = 'new')
844 def delete_patch(self, name):
847 self.__patch_name_valid(name)
848 patch = Patch(name, self.__patch_dir, self.__refs_dir)
850 if self.__patch_is_current(patch):
852 elif self.patch_applied(name):
853 raise StackException, 'Cannot remove an applied patch, "%s", ' \
854 'which is not current' % name
855 elif not name in self.get_unapplied():
856 raise StackException, 'Unknown patch "%s"' % name
858 # save the commit id to a trash file
859 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
863 unapplied = self.get_unapplied()
864 unapplied.remove(name)
865 f = file(self.__unapplied_file, 'w+')
866 f.writelines([line + '\n' for line in unapplied])
869 if self.patch_hidden(name):
870 self.unhide_patch(name)
872 def forward_patches(self, names):
873 """Try to fast-forward an array of patches.
875 On return, patches in names[0:returned_value] have been pushed on the
876 stack. Apply the rest with push_patch
878 unapplied = self.get_unapplied()
884 assert(name in unapplied)
886 patch = Patch(name, self.__patch_dir, self.__refs_dir)
889 bottom = patch.get_bottom()
890 top = patch.get_top()
892 # top != bottom always since we have a commit for each patch
894 # reset the backup information. No logging since the
895 # patch hasn't changed
896 patch.set_bottom(head, backup = True)
897 patch.set_top(top, backup = True)
900 head_tree = git.get_commit(head).get_tree()
901 bottom_tree = git.get_commit(bottom).get_tree()
902 if head_tree == bottom_tree:
903 # We must just reparent this patch and create a new commit
905 descr = patch.get_description()
906 author_name = patch.get_authname()
907 author_email = patch.get_authemail()
908 author_date = patch.get_authdate()
909 committer_name = patch.get_commname()
910 committer_email = patch.get_commemail()
912 top_tree = git.get_commit(top).get_tree()
914 top = git.commit(message = descr, parents = [head],
915 cache_update = False,
918 author_name = author_name,
919 author_email = author_email,
920 author_date = author_date,
921 committer_name = committer_name,
922 committer_email = committer_email)
924 patch.set_bottom(head, backup = True)
925 patch.set_top(top, backup = True)
927 self.log_patch(patch, 'push(f)')
930 # stop the fast-forwarding, must do a real merge
934 unapplied.remove(name)
941 append_strings(self.__applied_file, names[0:forwarded])
943 f = file(self.__unapplied_file, 'w+')
944 f.writelines([line + '\n' for line in unapplied])
947 self.__set_current(name)
951 def merged_patches(self, names):
952 """Test which patches were merged upstream by reverse-applying
953 them in reverse order. The function returns the list of
954 patches detected to have been applied. The state of the tree
955 is restored to the original one
957 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
963 if git.apply_diff(p.get_top(), p.get_bottom()):
964 merged.append(p.get_name())
971 def push_patch(self, name, empty = False):
972 """Pushes a patch on the stack
974 unapplied = self.get_unapplied()
975 assert(name in unapplied)
977 patch = Patch(name, self.__patch_dir, self.__refs_dir)
979 head = git.get_head()
980 bottom = patch.get_bottom()
981 top = patch.get_top()
986 # top != bottom always since we have a commit for each patch
988 # just make an empty patch (top = bottom = HEAD). This
989 # option is useful to allow undoing already merged
990 # patches. The top is updated by refresh_patch since we
991 # need an empty commit
992 patch.set_bottom(head, backup = True)
993 patch.set_top(head, backup = True)
996 # reset the backup information. No need for logging
997 patch.set_bottom(bottom, backup = True)
998 patch.set_top(top, backup = True)
1002 # new patch needs to be refreshed.
1003 # The current patch is empty after merge.
1004 patch.set_bottom(head, backup = True)
1005 patch.set_top(head, backup = True)
1007 # Try the fast applying first. If this fails, fall back to the
1009 if not git.apply_diff(bottom, top):
1010 # if git.apply_diff() fails, the patch requires a diff3
1011 # merge and can be reported as modified
1014 # merge can fail but the patch needs to be pushed
1016 git.merge(bottom, head, top, recursive = True)
1017 except git.GitException, ex:
1018 print >> sys.stderr, \
1019 'The merge failed during "push". ' \
1020 'Use "refresh" after fixing the conflicts or ' \
1021 'revert the operation with "push --undo".'
1023 append_string(self.__applied_file, name)
1025 unapplied.remove(name)
1026 f = file(self.__unapplied_file, 'w+')
1027 f.writelines([line + '\n' for line in unapplied])
1030 self.__set_current(name)
1032 # head == bottom case doesn't need to refresh the patch
1033 if empty or head != bottom:
1035 # if the merge was OK and no conflicts, just refresh the patch
1036 # The GIT cache was already updated by the merge operation
1041 self.refresh_patch(cache_update = False, log = log)
1043 # we store the correctly merged files only for
1044 # tracking the conflict history. Note that the
1045 # git.merge() operations should always leave the index
1046 # in a valid state (i.e. only stage 0 files)
1047 self.refresh_patch(cache_update = False, log = 'push(c)')
1048 raise StackException, str(ex)
1052 def undo_push(self):
1053 name = self.get_current()
1056 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1057 old_bottom = patch.get_old_bottom()
1058 old_top = patch.get_old_top()
1060 # the top of the patch is changed by a push operation only
1061 # together with the bottom (otherwise the top was probably
1062 # modified by 'refresh'). If they are both unchanged, there
1063 # was a fast forward
1064 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1065 raise StackException, 'No undo information available'
1068 self.pop_patch(name)
1069 ret = patch.restore_old_boundaries()
1071 self.log_patch(patch, 'undo')
1075 def pop_patch(self, name, keep = False):
1076 """Pops the top patch from the stack
1078 applied = self.get_applied()
1080 assert(name in applied)
1082 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1084 # only keep the local changes
1085 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1086 raise StackException, \
1087 'Failed to pop patches while preserving the local changes'
1089 git.switch(patch.get_bottom(), keep)
1091 # save the new applied list
1092 idx = applied.index(name) + 1
1094 popped = applied[:idx]
1096 unapplied = popped + self.get_unapplied()
1098 f = file(self.__unapplied_file, 'w+')
1099 f.writelines([line + '\n' for line in unapplied])
1105 f = file(self.__applied_file, 'w+')
1106 f.writelines([line + '\n' for line in applied])
1110 self.__set_current(None)
1112 self.__set_current(applied[-1])
1114 def empty_patch(self, name):
1115 """Returns True if the patch is empty
1117 self.__patch_name_valid(name)
1118 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1119 bottom = patch.get_bottom()
1120 top = patch.get_top()
1124 elif git.get_commit(top).get_tree() \
1125 == git.get_commit(bottom).get_tree():
1130 def rename_patch(self, oldname, newname):
1131 self.__patch_name_valid(newname)
1133 applied = self.get_applied()
1134 unapplied = self.get_unapplied()
1136 if oldname == newname:
1137 raise StackException, '"To" name and "from" name are the same'
1139 if newname in applied or newname in unapplied:
1140 raise StackException, 'Patch "%s" already exists' % newname
1142 if self.patch_hidden(oldname):
1143 self.unhide_patch(oldname)
1144 self.hide_patch(newname)
1146 if oldname in unapplied:
1147 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1148 unapplied[unapplied.index(oldname)] = newname
1150 f = file(self.__unapplied_file, 'w+')
1151 f.writelines([line + '\n' for line in unapplied])
1153 elif oldname in applied:
1154 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1155 if oldname == self.get_current():
1156 self.__set_current(newname)
1158 applied[applied.index(oldname)] = newname
1160 f = file(self.__applied_file, 'w+')
1161 f.writelines([line + '\n' for line in applied])
1164 raise StackException, 'Unknown patch "%s"' % oldname
1166 def log_patch(self, patch, message):
1167 """Generate a log commit for a patch
1169 top = git.get_commit(patch.get_top())
1170 msg = '%s\t%s' % (message, top.get_id_hash())
1172 old_log = patch.get_log()
1178 log = git.commit(message = msg, parents = parents,
1179 cache_update = False, tree_id = top.get_tree(),
1183 def hide_patch(self, name):
1184 """Add the patch to the hidden list.
1186 if not self.patch_exists(name):
1187 raise StackException, 'Unknown patch "%s"' % name
1188 elif self.patch_hidden(name):
1189 raise StackException, 'Patch "%s" already hidden' % name
1191 append_string(self.__hidden_file, name)
1193 def unhide_patch(self, name):
1194 """Add the patch to the hidden list.
1196 if not self.patch_exists(name):
1197 raise StackException, 'Unknown patch "%s"' % name
1198 hidden = self.get_hidden()
1199 if not name in hidden:
1200 raise StackException, 'Patch "%s" not hidden' % name
1204 f = file(self.__hidden_file, 'w+')
1205 f.writelines([line + '\n' for line in hidden])