1 """Basic quilt-like functionality
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 from stgit.utils import *
24 from stgit import git, basedir, templates
25 from stgit.config import config
28 # stack exception class
29 class StackException(Exception):
34 self.should_print = True
35 def __call__(self, x, until_test, prefix):
37 self.should_print = False
39 return x[0:len(prefix)] != prefix
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
48 def __clean_comments(f):
49 """Removes lines marked for status in a commit file
53 # remove status-prefixed lines
56 patch_filter = FilterUntil()
57 until_test = lambda t: t == (__patch_prefix + '\n')
58 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
60 # remove empty lines at the end
61 while len(lines) != 0 and lines[-1] == '\n':
64 f.seek(0); f.truncate()
67 def edit_file(series, line, comment, show_patch = True):
68 fname = '.stgitmsg.txt'
69 tmpl = templates.get_template('patchdescr.tmpl')
78 print >> f, __comment_prefix, comment
79 print >> f, __comment_prefix, \
80 'Lines prefixed with "%s" will be automatically removed.' \
82 print >> f, __comment_prefix, \
83 'Trailing empty lines will be automatically removed.'
86 print >> f, __patch_prefix
87 # series.get_patch(series.get_current()).get_top()
88 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
90 #Vim modeline must be near the end.
91 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
95 editor = config.get('stgit.editor')
98 elif 'EDITOR' in os.environ:
99 editor = os.environ['EDITOR']
102 editor += ' %s' % fname
104 print 'Invoking the editor: "%s"...' % editor,
106 print 'done (exit code: %d)' % os.system(editor)
108 f = file(fname, 'r+')
124 """An object with stgit-like properties stored as files in a directory
126 def _set_dir(self, dir):
131 def create_empty_field(self, name):
132 create_empty_file(os.path.join(self.__dir, name))
134 def _get_field(self, name, multiline = False):
135 id_file = os.path.join(self.__dir, name)
136 if os.path.isfile(id_file):
137 line = read_string(id_file, multiline)
145 def _set_field(self, name, value, multiline = False):
146 fname = os.path.join(self.__dir, name)
147 if value and value != '':
148 write_string(fname, value, multiline)
149 elif os.path.isfile(fname):
153 class Patch(StgitObject):
154 """Basic patch implementation
156 def __init__(self, name, series_dir, refs_dir):
157 self.__series_dir = series_dir
159 self._set_dir(os.path.join(self.__series_dir, self.__name))
160 self.__refs_dir = refs_dir
161 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
162 self.__log_ref_file = os.path.join(self.__refs_dir,
163 self.__name + '.log')
166 os.mkdir(self._dir())
167 self.create_empty_field('bottom')
168 self.create_empty_field('top')
171 for f in os.listdir(self._dir()):
172 os.remove(os.path.join(self._dir(), f))
173 os.rmdir(self._dir())
174 os.remove(self.__top_ref_file)
175 if os.path.exists(self.__log_ref_file):
176 os.remove(self.__log_ref_file)
181 def rename(self, newname):
183 old_top_ref_file = self.__top_ref_file
184 old_log_ref_file = self.__log_ref_file
185 self.__name = newname
186 self._set_dir(os.path.join(self.__series_dir, self.__name))
187 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
188 self.__log_ref_file = os.path.join(self.__refs_dir,
189 self.__name + '.log')
191 os.rename(olddir, self._dir())
192 os.rename(old_top_ref_file, self.__top_ref_file)
193 if os.path.exists(old_log_ref_file):
194 os.rename(old_log_ref_file, self.__log_ref_file)
196 def __update_top_ref(self, ref):
197 write_string(self.__top_ref_file, ref)
199 def __update_log_ref(self, ref):
200 write_string(self.__log_ref_file, ref)
202 def update_top_ref(self):
205 self.__update_top_ref(top)
207 def get_old_bottom(self):
208 return self._get_field('bottom.old')
210 def get_bottom(self):
211 return self._get_field('bottom')
213 def set_bottom(self, value, backup = False):
215 curr = self._get_field('bottom')
216 self._set_field('bottom.old', curr)
217 self._set_field('bottom', value)
219 def get_old_top(self):
220 return self._get_field('top.old')
223 return self._get_field('top')
225 def set_top(self, value, backup = False):
227 curr = self._get_field('top')
228 self._set_field('top.old', curr)
229 self._set_field('top', value)
230 self.__update_top_ref(value)
232 def restore_old_boundaries(self):
233 bottom = self._get_field('bottom.old')
234 top = self._get_field('top.old')
237 self._set_field('bottom', bottom)
238 self._set_field('top', top)
239 self.__update_top_ref(top)
244 def get_description(self):
245 return self._get_field('description', True)
247 def set_description(self, line):
248 self._set_field('description', line, True)
250 def get_authname(self):
251 return self._get_field('authname')
253 def set_authname(self, name):
254 self._set_field('authname', name or git.author().name)
256 def get_authemail(self):
257 return self._get_field('authemail')
259 def set_authemail(self, email):
260 self._set_field('authemail', email or git.author().email)
262 def get_authdate(self):
263 return self._get_field('authdate')
265 def set_authdate(self, date):
266 self._set_field('authdate', date or git.author().date)
268 def get_commname(self):
269 return self._get_field('commname')
271 def set_commname(self, name):
272 self._set_field('commname', name or git.committer().name)
274 def get_commemail(self):
275 return self._get_field('commemail')
277 def set_commemail(self, email):
278 self._set_field('commemail', email or git.committer().email)
281 return self._get_field('log')
283 def set_log(self, value, backup = False):
284 self._set_field('log', value)
285 self.__update_log_ref(value)
288 class Series(StgitObject):
289 """Class including the operations on series
291 def __init__(self, name = None):
292 """Takes a series name as the parameter.
298 self.__name = git.get_head_file()
299 self.__base_dir = basedir.get()
300 except git.GitException, ex:
301 raise StackException, 'GIT tree not initialised: %s' % ex
303 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
304 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
306 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
309 self.__applied_file = os.path.join(self._dir(), 'applied')
310 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
311 self.__hidden_file = os.path.join(self._dir(), 'hidden')
312 self.__current_file = os.path.join(self._dir(), 'current')
313 self.__descr_file = os.path.join(self._dir(), 'description')
315 # where this series keeps its patches
316 self.__patch_dir = os.path.join(self._dir(), 'patches')
317 if not os.path.isdir(self.__patch_dir):
318 self.__patch_dir = self._dir()
320 # if no __refs_dir, create and populate it (upgrade old repositories)
321 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
322 os.makedirs(self.__refs_dir)
323 for patch in self.get_applied() + self.get_unapplied():
324 self.get_patch(patch).update_top_ref()
327 self.__trash_dir = os.path.join(self._dir(), 'trash')
328 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
329 os.makedirs(self.__trash_dir)
331 def __patch_name_valid(self, name):
332 """Raise an exception if the patch name is not valid.
334 if not name or re.search('[^\w.-]', name):
335 raise StackException, 'Invalid patch name: "%s"' % name
337 def get_branch(self):
338 """Return the branch name for the Series object
342 def __set_current(self, name):
343 """Sets the topmost patch
345 self._set_field('current', name)
347 def get_patch(self, name):
348 """Return a Patch object for the given name
350 return Patch(name, self.__patch_dir, self.__refs_dir)
352 def get_current_patch(self):
353 """Return a Patch object representing the topmost patch, or
354 None if there is no such patch."""
355 crt = self.get_current()
358 return Patch(crt, self.__patch_dir, self.__refs_dir)
360 def get_current(self):
361 """Return the name of the topmost patch, or None if there is
363 name = self._get_field('current')
369 def get_applied(self):
370 if not os.path.isfile(self.__applied_file):
371 raise StackException, 'Branch "%s" not initialised' % self.__name
372 f = file(self.__applied_file)
373 names = [line.strip() for line in f.readlines()]
377 def get_unapplied(self):
378 if not os.path.isfile(self.__unapplied_file):
379 raise StackException, 'Branch "%s" not initialised' % self.__name
380 f = file(self.__unapplied_file)
381 names = [line.strip() for line in f.readlines()]
385 def get_hidden(self):
386 if not os.path.isfile(self.__hidden_file):
388 f = file(self.__hidden_file)
389 names = [line.strip() for line in f.readlines()]
393 def get_base_file(self):
394 self.__begin_stack_check()
395 return self.__base_file
397 def get_protected(self):
398 return os.path.isfile(os.path.join(self._dir(), 'protected'))
401 protect_file = os.path.join(self._dir(), 'protected')
402 if not os.path.isfile(protect_file):
403 create_empty_file(protect_file)
406 protect_file = os.path.join(self._dir(), 'protected')
407 if os.path.isfile(protect_file):
408 os.remove(protect_file)
410 def get_description(self):
411 return self._get_field('description') or ''
413 def set_description(self, line):
414 self._set_field('description', line)
416 def get_parent_remote(self):
417 value = config.get('branch.%s.remote' % self.__name)
420 elif 'origin' in git.remotes_list():
421 print 'Notice: no parent remote declared for stack "%s", defaulting to "origin".' \
422 ' Consider setting "branch.%s.remote" with "git repo-config".' \
423 % (self.__name, self.__name)
426 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
428 def __set_parent_remote(self, remote):
429 value = config.set('branch.%s.remote' % self.__name, remote)
431 def get_parent_branch(self):
432 value = config.get('branch.%s.merge' % self.__name)
435 elif git.rev_parse('heads/origin'):
436 print 'Notice: no parent branch declared for stack "%s", defaulting to "heads/origin".' \
437 ' Consider setting "branch.%s.merge" with "git repo-config".' \
438 % (self.__name, self.__name)
439 return 'heads/origin'
441 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
443 def __set_parent_branch(self, name):
444 config.set('branch.%s.merge' % self.__name, name)
446 def set_parent(self, remote, localbranch):
448 self.__set_parent_branch(localbranch)
450 self.__set_parent_remote(remote)
452 raise StackException, 'Remote "%s" without a branch cannot be used as parent' % remote
454 def __patch_is_current(self, patch):
455 return patch.get_name() == self.get_current()
457 def patch_applied(self, name):
458 """Return true if the patch exists in the applied list
460 return name in self.get_applied()
462 def patch_unapplied(self, name):
463 """Return true if the patch exists in the unapplied list
465 return name in self.get_unapplied()
467 def patch_hidden(self, name):
468 """Return true if the patch is hidden.
470 return name in self.get_hidden()
472 def patch_exists(self, name):
473 """Return true if there is a patch with the given name, false
475 return self.patch_applied(name) or self.patch_unapplied(name)
477 def __begin_stack_check(self):
478 """Save the current HEAD into .git/refs/heads/base if the stack
481 if len(self.get_applied()) == 0:
482 head = git.get_head()
483 write_string(self.__base_file, head)
485 def __end_stack_check(self):
486 """Remove .git/refs/heads/base if the stack is empty.
487 This warning should never happen
489 if len(self.get_applied()) == 0 \
490 and read_string(self.__base_file) != git.get_head():
491 print 'Warning: stack empty but the HEAD and base are different'
493 def head_top_equal(self):
494 """Return true if the head and the top are the same
496 crt = self.get_current_patch()
498 # we don't care, no patches applied
500 return git.get_head() == crt.get_top()
502 def is_initialised(self):
503 """Checks if series is already initialised
505 return os.path.isdir(self.__patch_dir)
507 def init(self, create_at=False, parent_remote=None, parent_branch=None):
508 """Initialises the stgit series
510 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
512 if os.path.exists(self.__patch_dir):
513 raise StackException, self.__patch_dir + ' already exists'
514 if os.path.exists(self.__refs_dir):
515 raise StackException, self.__refs_dir + ' already exists'
516 if os.path.exists(self.__base_file):
517 raise StackException, self.__base_file + ' already exists'
519 if (create_at!=False):
520 git.create_branch(self.__name, create_at)
522 os.makedirs(self.__patch_dir)
524 self.set_parent(parent_remote, parent_branch)
526 create_dirs(bases_dir)
528 self.create_empty_field('applied')
529 self.create_empty_field('unapplied')
530 self.create_empty_field('description')
531 os.makedirs(os.path.join(self._dir(), 'patches'))
532 os.makedirs(self.__refs_dir)
533 self.__begin_stack_check()
536 """Either convert to use a separate patch directory, or
537 unconvert to place the patches in the same directory with
540 if self.__patch_dir == self._dir():
541 print 'Converting old-style to new-style...',
544 self.__patch_dir = os.path.join(self._dir(), 'patches')
545 os.makedirs(self.__patch_dir)
547 for p in self.get_applied() + self.get_unapplied():
548 src = os.path.join(self._dir(), p)
549 dest = os.path.join(self.__patch_dir, p)
555 print 'Converting new-style to old-style...',
558 for p in self.get_applied() + self.get_unapplied():
559 src = os.path.join(self.__patch_dir, p)
560 dest = os.path.join(self._dir(), p)
563 if not os.listdir(self.__patch_dir):
564 os.rmdir(self.__patch_dir)
567 print 'Patch directory %s is not empty.' % self.__name
569 self.__patch_dir = self._dir()
571 def rename(self, to_name):
574 to_stack = Series(to_name)
576 if to_stack.is_initialised():
577 raise StackException, '"%s" already exists' % to_stack.get_branch()
578 if os.path.exists(to_stack.__base_file):
579 os.remove(to_stack.__base_file)
581 git.rename_branch(self.__name, to_name)
583 if os.path.isdir(self._dir()):
584 rename(os.path.join(self.__base_dir, 'patches'),
585 self.__name, to_stack.__name)
586 if os.path.exists(self.__base_file):
587 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
588 self.__name, to_stack.__name)
589 if os.path.exists(self.__refs_dir):
590 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
591 self.__name, to_stack.__name)
593 # Rename the config section
594 config.rename_section("branch.%s" % self.__name,
595 "branch.%s" % to_name)
597 self.__init__(to_name)
599 def clone(self, target_series):
603 # allow cloning of branches not under StGIT control
604 base = read_string(self.get_base_file())
606 base = git.get_head()
607 Series(target_series).init(create_at = base)
608 new_series = Series(target_series)
610 # generate an artificial description file
611 new_series.set_description('clone of "%s"' % self.__name)
613 # clone self's entire series as unapplied patches
615 # allow cloning of branches not under StGIT control
616 applied = self.get_applied()
617 unapplied = self.get_unapplied()
618 patches = applied + unapplied
621 patches = applied = unapplied = []
623 patch = self.get_patch(p)
624 new_series.new_patch(p, message = patch.get_description(),
625 can_edit = False, unapplied = True,
626 bottom = patch.get_bottom(),
627 top = patch.get_top(),
628 author_name = patch.get_authname(),
629 author_email = patch.get_authemail(),
630 author_date = patch.get_authdate())
632 # fast forward the cloned series to self's top
633 new_series.forward_patches(applied)
635 # Clone remote and merge settings
636 value = config.get('branch.%s.remote' % self.__name)
638 config.set('branch.%s.remote' % target_series, value)
640 value = config.get('branch.%s.merge' % self.__name)
642 config.set('branch.%s.merge' % target_series, value)
644 def delete(self, force = False):
645 """Deletes an stgit series
647 if self.is_initialised():
648 patches = self.get_unapplied() + self.get_applied()
649 if not force and patches:
650 raise StackException, \
651 'Cannot delete: the series still contains patches'
653 Patch(p, self.__patch_dir, self.__refs_dir).delete()
655 # remove the trash directory
656 for fname in os.listdir(self.__trash_dir):
658 os.rmdir(self.__trash_dir)
660 # FIXME: find a way to get rid of those manual removals
661 # (move functionality to StgitObject ?)
662 if os.path.exists(self.__applied_file):
663 os.remove(self.__applied_file)
664 if os.path.exists(self.__unapplied_file):
665 os.remove(self.__unapplied_file)
666 if os.path.exists(self.__hidden_file):
667 os.remove(self.__hidden_file)
668 if os.path.exists(self.__current_file):
669 os.remove(self.__current_file)
670 if os.path.exists(self.__descr_file):
671 os.remove(self.__descr_file)
672 if not os.listdir(self.__patch_dir):
673 os.rmdir(self.__patch_dir)
675 print 'Patch directory %s is not empty.' % self.__name
676 if not os.listdir(self._dir()):
677 remove_dirs(os.path.join(self.__base_dir, 'patches'),
680 print 'Series directory %s is not empty.' % self.__name
681 if not os.listdir(self.__refs_dir):
682 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
685 print 'Refs directory %s is not empty.' % self.__refs_dir
687 if os.path.exists(self.__base_file):
688 remove_file_and_dirs(
689 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
691 def refresh_patch(self, files = None, message = None, edit = False,
694 author_name = None, author_email = None,
696 committer_name = None, committer_email = None,
697 backup = False, sign_str = None, log = 'refresh'):
698 """Generates a new commit for the given patch
700 name = self.get_current()
702 raise StackException, 'No patches applied'
704 patch = Patch(name, self.__patch_dir, self.__refs_dir)
706 descr = patch.get_description()
707 if not (message or descr):
713 if not message and edit:
714 descr = edit_file(self, descr.rstrip(), \
715 'Please edit the description for patch "%s" ' \
716 'above.' % name, show_patch)
719 author_name = patch.get_authname()
721 author_email = patch.get_authemail()
723 author_date = patch.get_authdate()
724 if not committer_name:
725 committer_name = patch.get_commname()
726 if not committer_email:
727 committer_email = patch.get_commemail()
730 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
731 committer_name, committer_email)
733 bottom = patch.get_bottom()
735 commit_id = git.commit(files = files,
736 message = descr, parents = [bottom],
737 cache_update = cache_update,
739 author_name = author_name,
740 author_email = author_email,
741 author_date = author_date,
742 committer_name = committer_name,
743 committer_email = committer_email)
745 patch.set_bottom(bottom, backup = backup)
746 patch.set_top(commit_id, backup = backup)
747 patch.set_description(descr)
748 patch.set_authname(author_name)
749 patch.set_authemail(author_email)
750 patch.set_authdate(author_date)
751 patch.set_commname(committer_name)
752 patch.set_commemail(committer_email)
755 self.log_patch(patch, log)
759 def undo_refresh(self):
760 """Undo the patch boundaries changes caused by 'refresh'
762 name = self.get_current()
765 patch = Patch(name, self.__patch_dir, self.__refs_dir)
766 old_bottom = patch.get_old_bottom()
767 old_top = patch.get_old_top()
769 # the bottom of the patch is not changed by refresh. If the
770 # old_bottom is different, there wasn't any previous 'refresh'
771 # command (probably only a 'push')
772 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
773 raise StackException, 'No undo information available'
775 git.reset(tree_id = old_top, check_out = False)
776 if patch.restore_old_boundaries():
777 self.log_patch(patch, 'undo')
779 def new_patch(self, name, message = None, can_edit = True,
780 unapplied = False, show_patch = False,
781 top = None, bottom = None,
782 author_name = None, author_email = None, author_date = None,
783 committer_name = None, committer_email = None,
784 before_existing = False, refresh = True):
785 """Creates a new patch
787 self.__patch_name_valid(name)
789 if self.patch_applied(name) or self.patch_unapplied(name):
790 raise StackException, 'Patch "%s" already exists' % name
792 if not message and can_edit:
793 descr = edit_file(self, None, \
794 'Please enter the description for patch "%s" ' \
795 'above.' % name, show_patch)
799 head = git.get_head()
801 self.__begin_stack_check()
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')
842 def delete_patch(self, name):
845 self.__patch_name_valid(name)
846 patch = Patch(name, self.__patch_dir, self.__refs_dir)
848 if self.__patch_is_current(patch):
850 elif self.patch_applied(name):
851 raise StackException, 'Cannot remove an applied patch, "%s", ' \
852 'which is not current' % name
853 elif not name in self.get_unapplied():
854 raise StackException, 'Unknown patch "%s"' % name
856 # save the commit id to a trash file
857 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
861 unapplied = self.get_unapplied()
862 unapplied.remove(name)
863 f = file(self.__unapplied_file, 'w+')
864 f.writelines([line + '\n' for line in unapplied])
867 if self.patch_hidden(name):
868 self.unhide_patch(name)
870 self.__begin_stack_check()
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()
879 self.__begin_stack_check()
885 assert(name in unapplied)
887 patch = Patch(name, self.__patch_dir, self.__refs_dir)
890 bottom = patch.get_bottom()
891 top = patch.get_top()
893 # top != bottom always since we have a commit for each patch
895 # reset the backup information. No logging since the
896 # patch hasn't changed
897 patch.set_bottom(head, backup = True)
898 patch.set_top(top, backup = True)
901 head_tree = git.get_commit(head).get_tree()
902 bottom_tree = git.get_commit(bottom).get_tree()
903 if head_tree == bottom_tree:
904 # We must just reparent this patch and create a new commit
906 descr = patch.get_description()
907 author_name = patch.get_authname()
908 author_email = patch.get_authemail()
909 author_date = patch.get_authdate()
910 committer_name = patch.get_commname()
911 committer_email = patch.get_commemail()
913 top_tree = git.get_commit(top).get_tree()
915 top = git.commit(message = descr, parents = [head],
916 cache_update = False,
919 author_name = author_name,
920 author_email = author_email,
921 author_date = author_date,
922 committer_name = committer_name,
923 committer_email = committer_email)
925 patch.set_bottom(head, backup = True)
926 patch.set_top(top, backup = True)
928 self.log_patch(patch, 'push(f)')
931 # stop the fast-forwarding, must do a real merge
935 unapplied.remove(name)
942 append_strings(self.__applied_file, names[0:forwarded])
944 f = file(self.__unapplied_file, 'w+')
945 f.writelines([line + '\n' for line in unapplied])
948 self.__set_current(name)
952 def merged_patches(self, names):
953 """Test which patches were merged upstream by reverse-applying
954 them in reverse order. The function returns the list of
955 patches detected to have been applied. The state of the tree
956 is restored to the original one
958 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
964 if git.apply_diff(p.get_top(), p.get_bottom()):
965 merged.append(p.get_name())
972 def push_patch(self, name, empty = False):
973 """Pushes a patch on the stack
975 unapplied = self.get_unapplied()
976 assert(name in unapplied)
978 self.__begin_stack_check()
980 patch = Patch(name, self.__patch_dir, self.__refs_dir)
982 head = git.get_head()
983 bottom = patch.get_bottom()
984 top = patch.get_top()
989 # top != bottom always since we have a commit for each patch
991 # just make an empty patch (top = bottom = HEAD). This
992 # option is useful to allow undoing already merged
993 # patches. The top is updated by refresh_patch since we
994 # need an empty commit
995 patch.set_bottom(head, backup = True)
996 patch.set_top(head, backup = True)
999 # reset the backup information. No need for logging
1000 patch.set_bottom(bottom, backup = True)
1001 patch.set_top(top, backup = True)
1005 # new patch needs to be refreshed.
1006 # The current patch is empty after merge.
1007 patch.set_bottom(head, backup = True)
1008 patch.set_top(head, backup = True)
1010 # Try the fast applying first. If this fails, fall back to the
1012 if not git.apply_diff(bottom, top):
1013 # if git.apply_diff() fails, the patch requires a diff3
1014 # merge and can be reported as modified
1017 # merge can fail but the patch needs to be pushed
1019 git.merge(bottom, head, top, recursive = True)
1020 except git.GitException, ex:
1021 print >> sys.stderr, \
1022 'The merge failed during "push". ' \
1023 'Use "refresh" after fixing the conflicts'
1025 append_string(self.__applied_file, name)
1027 unapplied.remove(name)
1028 f = file(self.__unapplied_file, 'w+')
1029 f.writelines([line + '\n' for line in unapplied])
1032 self.__set_current(name)
1034 # head == bottom case doesn't need to refresh the patch
1035 if empty or head != bottom:
1037 # if the merge was OK and no conflicts, just refresh the patch
1038 # The GIT cache was already updated by the merge operation
1043 self.refresh_patch(cache_update = False, log = log)
1045 # we store the correctly merged files only for
1046 # tracking the conflict history. Note that the
1047 # git.merge() operations should always leave the index
1048 # in a valid state (i.e. only stage 0 files)
1049 self.refresh_patch(cache_update = False, log = 'push(c)')
1050 raise StackException, str(ex)
1054 def undo_push(self):
1055 name = self.get_current()
1058 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1059 old_bottom = patch.get_old_bottom()
1060 old_top = patch.get_old_top()
1062 # the top of the patch is changed by a push operation only
1063 # together with the bottom (otherwise the top was probably
1064 # modified by 'refresh'). If they are both unchanged, there
1065 # was a fast forward
1066 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1067 raise StackException, 'No undo information available'
1070 self.pop_patch(name)
1071 ret = patch.restore_old_boundaries()
1073 self.log_patch(patch, 'undo')
1077 def pop_patch(self, name, keep = False):
1078 """Pops the top patch from the stack
1080 applied = self.get_applied()
1082 assert(name in applied)
1084 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1086 # only keep the local changes
1087 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1088 raise StackException, \
1089 'Failed to pop patches while preserving the local changes'
1091 git.switch(patch.get_bottom(), keep)
1093 # save the new applied list
1094 idx = applied.index(name) + 1
1096 popped = applied[:idx]
1098 unapplied = popped + self.get_unapplied()
1100 f = file(self.__unapplied_file, 'w+')
1101 f.writelines([line + '\n' for line in unapplied])
1107 f = file(self.__applied_file, 'w+')
1108 f.writelines([line + '\n' for line in applied])
1112 self.__set_current(None)
1114 self.__set_current(applied[-1])
1116 self.__end_stack_check()
1118 def empty_patch(self, name):
1119 """Returns True if the patch is empty
1121 self.__patch_name_valid(name)
1122 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1123 bottom = patch.get_bottom()
1124 top = patch.get_top()
1128 elif git.get_commit(top).get_tree() \
1129 == git.get_commit(bottom).get_tree():
1134 def rename_patch(self, oldname, newname):
1135 self.__patch_name_valid(newname)
1137 applied = self.get_applied()
1138 unapplied = self.get_unapplied()
1140 if oldname == newname:
1141 raise StackException, '"To" name and "from" name are the same'
1143 if newname in applied or newname in unapplied:
1144 raise StackException, 'Patch "%s" already exists' % newname
1146 if self.patch_hidden(oldname):
1147 self.unhide_patch(oldname)
1148 self.hide_patch(newname)
1150 if oldname in unapplied:
1151 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1152 unapplied[unapplied.index(oldname)] = newname
1154 f = file(self.__unapplied_file, 'w+')
1155 f.writelines([line + '\n' for line in unapplied])
1157 elif oldname in applied:
1158 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1159 if oldname == self.get_current():
1160 self.__set_current(newname)
1162 applied[applied.index(oldname)] = newname
1164 f = file(self.__applied_file, 'w+')
1165 f.writelines([line + '\n' for line in applied])
1168 raise StackException, 'Unknown patch "%s"' % oldname
1170 def log_patch(self, patch, message):
1171 """Generate a log commit for a patch
1173 top = git.get_commit(patch.get_top())
1174 msg = '%s\t%s' % (message, top.get_id_hash())
1176 old_log = patch.get_log()
1182 log = git.commit(message = msg, parents = parents,
1183 cache_update = False, tree_id = top.get_tree(),
1187 def hide_patch(self, name):
1188 """Add the patch to the hidden list.
1190 if not self.patch_exists(name):
1191 raise StackException, 'Unknown patch "%s"' % name
1192 elif self.patch_hidden(name):
1193 raise StackException, 'Patch "%s" already hidden' % name
1195 append_string(self.__hidden_file, name)
1197 def unhide_patch(self, name):
1198 """Add the patch to the hidden list.
1200 if not self.patch_exists(name):
1201 raise StackException, 'Unknown patch "%s"' % name
1202 hidden = self.get_hidden()
1203 if not name in hidden:
1204 raise StackException, 'Patch "%s" not hidden' % name
1208 f = file(self.__hidden_file, 'w+')
1209 f.writelines([line + '\n' for line in hidden])