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 get_branch(self):
332 """Return the branch name for the Series object
336 def __set_current(self, name):
337 """Sets the topmost patch
339 self._set_field('current', name)
341 def get_patch(self, name):
342 """Return a Patch object for the given name
344 return Patch(name, self.__patch_dir, self.__refs_dir)
346 def get_current_patch(self):
347 """Return a Patch object representing the topmost patch, or
348 None if there is no such patch."""
349 crt = self.get_current()
352 return Patch(crt, self.__patch_dir, self.__refs_dir)
354 def get_current(self):
355 """Return the name of the topmost patch, or None if there is
357 name = self._get_field('current')
363 def get_applied(self):
364 if not os.path.isfile(self.__applied_file):
365 raise StackException, 'Branch "%s" not initialised' % self.__name
366 f = file(self.__applied_file)
367 names = [line.strip() for line in f.readlines()]
371 def get_unapplied(self):
372 if not os.path.isfile(self.__unapplied_file):
373 raise StackException, 'Branch "%s" not initialised' % self.__name
374 f = file(self.__unapplied_file)
375 names = [line.strip() for line in f.readlines()]
379 def get_hidden(self):
380 if not os.path.isfile(self.__hidden_file):
382 f = file(self.__hidden_file)
383 names = [line.strip() for line in f.readlines()]
387 def get_base_file(self):
388 self.__begin_stack_check()
389 return self.__base_file
391 def get_protected(self):
392 return os.path.isfile(os.path.join(self._dir(), 'protected'))
395 protect_file = os.path.join(self._dir(), 'protected')
396 if not os.path.isfile(protect_file):
397 create_empty_file(protect_file)
400 protect_file = os.path.join(self._dir(), 'protected')
401 if os.path.isfile(protect_file):
402 os.remove(protect_file)
404 def get_description(self):
405 return self._get_field('description') or ''
407 def set_description(self, line):
408 self._set_field('description', line)
410 def get_parent_remote(self):
411 return config.get('branch.%s.remote' % self.__name) or 'origin'
413 def __set_parent_remote(self, remote):
414 value = config.set('branch.%s.remote' % self.__name, remote)
416 def __patch_is_current(self, patch):
417 return patch.get_name() == self.get_current()
419 def patch_applied(self, name):
420 """Return true if the patch exists in the applied list
422 return name in self.get_applied()
424 def patch_unapplied(self, name):
425 """Return true if the patch exists in the unapplied list
427 return name in self.get_unapplied()
429 def patch_hidden(self, name):
430 """Return true if the patch is hidden.
432 return name in self.get_hidden()
434 def patch_exists(self, name):
435 """Return true if there is a patch with the given name, false
437 return self.patch_applied(name) or self.patch_unapplied(name)
439 def __begin_stack_check(self):
440 """Save the current HEAD into .git/refs/heads/base if the stack
443 if len(self.get_applied()) == 0:
444 head = git.get_head()
445 write_string(self.__base_file, head)
447 def __end_stack_check(self):
448 """Remove .git/refs/heads/base if the stack is empty.
449 This warning should never happen
451 if len(self.get_applied()) == 0 \
452 and read_string(self.__base_file) != git.get_head():
453 print 'Warning: stack empty but the HEAD and base are different'
455 def head_top_equal(self):
456 """Return true if the head and the top are the same
458 crt = self.get_current_patch()
460 # we don't care, no patches applied
462 return git.get_head() == crt.get_top()
464 def is_initialised(self):
465 """Checks if series is already initialised
467 return os.path.isdir(self.__patch_dir)
469 def init(self, create_at=False):
470 """Initialises the stgit series
472 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
474 if os.path.exists(self.__patch_dir):
475 raise StackException, self.__patch_dir + ' already exists'
476 if os.path.exists(self.__refs_dir):
477 raise StackException, self.__refs_dir + ' already exists'
478 if os.path.exists(self.__base_file):
479 raise StackException, self.__base_file + ' already exists'
481 if (create_at!=False):
482 git.create_branch(self.__name, create_at)
484 os.makedirs(self.__patch_dir)
486 create_dirs(bases_dir)
488 self.create_empty_field('applied')
489 self.create_empty_field('unapplied')
490 self.create_empty_field('description')
491 os.makedirs(os.path.join(self._dir(), 'patches'))
492 os.makedirs(self.__refs_dir)
493 self.__begin_stack_check()
496 """Either convert to use a separate patch directory, or
497 unconvert to place the patches in the same directory with
500 if self.__patch_dir == self._dir():
501 print 'Converting old-style to new-style...',
504 self.__patch_dir = os.path.join(self._dir(), 'patches')
505 os.makedirs(self.__patch_dir)
507 for p in self.get_applied() + self.get_unapplied():
508 src = os.path.join(self._dir(), p)
509 dest = os.path.join(self.__patch_dir, p)
515 print 'Converting new-style to old-style...',
518 for p in self.get_applied() + self.get_unapplied():
519 src = os.path.join(self.__patch_dir, p)
520 dest = os.path.join(self._dir(), p)
523 if not os.listdir(self.__patch_dir):
524 os.rmdir(self.__patch_dir)
527 print 'Patch directory %s is not empty.' % self.__name
529 self.__patch_dir = self._dir()
531 def rename(self, to_name):
534 to_stack = Series(to_name)
536 if to_stack.is_initialised():
537 raise StackException, '"%s" already exists' % to_stack.get_branch()
538 if os.path.exists(to_stack.__base_file):
539 os.remove(to_stack.__base_file)
541 git.rename_branch(self.__name, to_name)
543 if os.path.isdir(self._dir()):
544 rename(os.path.join(self.__base_dir, 'patches'),
545 self.__name, to_stack.__name)
546 if os.path.exists(self.__base_file):
547 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
548 self.__name, to_stack.__name)
549 if os.path.exists(self.__refs_dir):
550 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
551 self.__name, to_stack.__name)
553 self.__init__(to_name)
555 def clone(self, target_series):
559 # allow cloning of branches not under StGIT control
560 base = read_string(self.get_base_file())
562 base = git.get_head()
563 Series(target_series).init(create_at = base)
564 new_series = Series(target_series)
566 # generate an artificial description file
567 new_series.set_description('clone of "%s"' % self.__name)
569 # clone self's entire series as unapplied patches
571 # allow cloning of branches not under StGIT control
572 applied = self.get_applied()
573 unapplied = self.get_unapplied()
574 patches = applied + unapplied
577 patches = applied = unapplied = []
579 patch = self.get_patch(p)
580 new_series.new_patch(p, message = patch.get_description(),
581 can_edit = False, unapplied = True,
582 bottom = patch.get_bottom(),
583 top = patch.get_top(),
584 author_name = patch.get_authname(),
585 author_email = patch.get_authemail(),
586 author_date = patch.get_authdate())
588 # fast forward the cloned series to self's top
589 new_series.forward_patches(applied)
591 def delete(self, force = False):
592 """Deletes an stgit series
594 if self.is_initialised():
595 patches = self.get_unapplied() + self.get_applied()
596 if not force and patches:
597 raise StackException, \
598 'Cannot delete: the series still contains patches'
600 Patch(p, self.__patch_dir, self.__refs_dir).delete()
602 # remove the trash directory
603 for fname in os.listdir(self.__trash_dir):
605 os.rmdir(self.__trash_dir)
607 # FIXME: find a way to get rid of those manual removals
608 # (move functionnality to StgitObject ?)
609 if os.path.exists(self.__applied_file):
610 os.remove(self.__applied_file)
611 if os.path.exists(self.__unapplied_file):
612 os.remove(self.__unapplied_file)
613 if os.path.exists(self.__hidden_file):
614 os.remove(self.__hidden_file)
615 if os.path.exists(self.__current_file):
616 os.remove(self.__current_file)
617 if os.path.exists(self.__descr_file):
618 os.remove(self.__descr_file)
619 if not os.listdir(self.__patch_dir):
620 os.rmdir(self.__patch_dir)
622 print 'Patch directory %s is not empty.' % self.__name
623 if not os.listdir(self._dir()):
624 remove_dirs(os.path.join(self.__base_dir, 'patches'),
627 print 'Series directory %s is not empty.' % self.__name
628 if not os.listdir(self.__refs_dir):
629 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
632 print 'Refs directory %s is not empty.' % self.__refs_dir
634 if os.path.exists(self.__base_file):
635 remove_file_and_dirs(
636 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
638 def refresh_patch(self, files = None, message = None, edit = False,
641 author_name = None, author_email = None,
643 committer_name = None, committer_email = None,
644 backup = False, sign_str = None, log = 'refresh'):
645 """Generates a new commit for the given patch
647 name = self.get_current()
649 raise StackException, 'No patches applied'
651 patch = Patch(name, self.__patch_dir, self.__refs_dir)
653 descr = patch.get_description()
654 if not (message or descr):
660 if not message and edit:
661 descr = edit_file(self, descr.rstrip(), \
662 'Please edit the description for patch "%s" ' \
663 'above.' % name, show_patch)
666 author_name = patch.get_authname()
668 author_email = patch.get_authemail()
670 author_date = patch.get_authdate()
671 if not committer_name:
672 committer_name = patch.get_commname()
673 if not committer_email:
674 committer_email = patch.get_commemail()
677 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
678 committer_name, committer_email)
680 bottom = patch.get_bottom()
682 commit_id = git.commit(files = files,
683 message = descr, parents = [bottom],
684 cache_update = cache_update,
686 author_name = author_name,
687 author_email = author_email,
688 author_date = author_date,
689 committer_name = committer_name,
690 committer_email = committer_email)
692 patch.set_bottom(bottom, backup = backup)
693 patch.set_top(commit_id, backup = backup)
694 patch.set_description(descr)
695 patch.set_authname(author_name)
696 patch.set_authemail(author_email)
697 patch.set_authdate(author_date)
698 patch.set_commname(committer_name)
699 patch.set_commemail(committer_email)
702 self.log_patch(patch, log)
706 def undo_refresh(self):
707 """Undo the patch boundaries changes caused by 'refresh'
709 name = self.get_current()
712 patch = Patch(name, self.__patch_dir, self.__refs_dir)
713 old_bottom = patch.get_old_bottom()
714 old_top = patch.get_old_top()
716 # the bottom of the patch is not changed by refresh. If the
717 # old_bottom is different, there wasn't any previous 'refresh'
718 # command (probably only a 'push')
719 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
720 raise StackException, 'No undo information available'
722 git.reset(tree_id = old_top, check_out = False)
723 if patch.restore_old_boundaries():
724 self.log_patch(patch, 'undo')
726 def new_patch(self, name, message = None, can_edit = True,
727 unapplied = False, show_patch = False,
728 top = None, bottom = None,
729 author_name = None, author_email = None, author_date = None,
730 committer_name = None, committer_email = None,
731 before_existing = False, refresh = True):
732 """Creates a new patch
734 if self.patch_applied(name) or self.patch_unapplied(name):
735 raise StackException, 'Patch "%s" already exists' % name
737 if not message and can_edit:
738 descr = edit_file(self, None, \
739 'Please enter the description for patch "%s" ' \
740 'above.' % name, show_patch)
744 head = git.get_head()
746 self.__begin_stack_check()
748 patch = Patch(name, self.__patch_dir, self.__refs_dir)
752 patch.set_bottom(bottom)
754 patch.set_bottom(head)
760 patch.set_description(descr)
761 patch.set_authname(author_name)
762 patch.set_authemail(author_email)
763 patch.set_authdate(author_date)
764 patch.set_commname(committer_name)
765 patch.set_commemail(committer_email)
768 self.log_patch(patch, 'new')
770 patches = [patch.get_name()] + self.get_unapplied()
772 f = file(self.__unapplied_file, 'w+')
773 f.writelines([line + '\n' for line in patches])
775 elif before_existing:
776 self.log_patch(patch, 'new')
778 insert_string(self.__applied_file, patch.get_name())
779 if not self.get_current():
780 self.__set_current(name)
782 append_string(self.__applied_file, patch.get_name())
783 self.__set_current(name)
785 self.refresh_patch(cache_update = False, log = 'new')
787 def delete_patch(self, name):
790 patch = Patch(name, self.__patch_dir, self.__refs_dir)
792 if self.__patch_is_current(patch):
794 elif self.patch_applied(name):
795 raise StackException, 'Cannot remove an applied patch, "%s", ' \
796 'which is not current' % name
797 elif not name in self.get_unapplied():
798 raise StackException, 'Unknown patch "%s"' % name
800 # save the commit id to a trash file
801 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
805 unapplied = self.get_unapplied()
806 unapplied.remove(name)
807 f = file(self.__unapplied_file, 'w+')
808 f.writelines([line + '\n' for line in unapplied])
811 if self.patch_hidden(name):
812 self.unhide_patch(name)
814 self.__begin_stack_check()
816 def forward_patches(self, names):
817 """Try to fast-forward an array of patches.
819 On return, patches in names[0:returned_value] have been pushed on the
820 stack. Apply the rest with push_patch
822 unapplied = self.get_unapplied()
823 self.__begin_stack_check()
829 assert(name in unapplied)
831 patch = Patch(name, self.__patch_dir, self.__refs_dir)
834 bottom = patch.get_bottom()
835 top = patch.get_top()
837 # top != bottom always since we have a commit for each patch
839 # reset the backup information. No logging since the
840 # patch hasn't changed
841 patch.set_bottom(head, backup = True)
842 patch.set_top(top, backup = True)
845 head_tree = git.get_commit(head).get_tree()
846 bottom_tree = git.get_commit(bottom).get_tree()
847 if head_tree == bottom_tree:
848 # We must just reparent this patch and create a new commit
850 descr = patch.get_description()
851 author_name = patch.get_authname()
852 author_email = patch.get_authemail()
853 author_date = patch.get_authdate()
854 committer_name = patch.get_commname()
855 committer_email = patch.get_commemail()
857 top_tree = git.get_commit(top).get_tree()
859 top = git.commit(message = descr, parents = [head],
860 cache_update = False,
863 author_name = author_name,
864 author_email = author_email,
865 author_date = author_date,
866 committer_name = committer_name,
867 committer_email = committer_email)
869 patch.set_bottom(head, backup = True)
870 patch.set_top(top, backup = True)
872 self.log_patch(patch, 'push(f)')
875 # stop the fast-forwarding, must do a real merge
879 unapplied.remove(name)
886 append_strings(self.__applied_file, names[0:forwarded])
888 f = file(self.__unapplied_file, 'w+')
889 f.writelines([line + '\n' for line in unapplied])
892 self.__set_current(name)
896 def merged_patches(self, names):
897 """Test which patches were merged upstream by reverse-applying
898 them in reverse order. The function returns the list of
899 patches detected to have been applied. The state of the tree
900 is restored to the original one
902 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
908 if git.apply_diff(p.get_top(), p.get_bottom()):
909 merged.append(p.get_name())
916 def push_patch(self, name, empty = False):
917 """Pushes a patch on the stack
919 unapplied = self.get_unapplied()
920 assert(name in unapplied)
922 self.__begin_stack_check()
924 patch = Patch(name, self.__patch_dir, self.__refs_dir)
926 head = git.get_head()
927 bottom = patch.get_bottom()
928 top = patch.get_top()
933 # top != bottom always since we have a commit for each patch
935 # just make an empty patch (top = bottom = HEAD). This
936 # option is useful to allow undoing already merged
937 # patches. The top is updated by refresh_patch since we
938 # need an empty commit
939 patch.set_bottom(head, backup = True)
940 patch.set_top(head, backup = True)
943 # reset the backup information. No need for logging
944 patch.set_bottom(bottom, backup = True)
945 patch.set_top(top, backup = True)
949 # new patch needs to be refreshed.
950 # The current patch is empty after merge.
951 patch.set_bottom(head, backup = True)
952 patch.set_top(head, backup = True)
954 # Try the fast applying first. If this fails, fall back to the
956 if not git.apply_diff(bottom, top):
957 # if git.apply_diff() fails, the patch requires a diff3
958 # merge and can be reported as modified
961 # merge can fail but the patch needs to be pushed
963 git.merge(bottom, head, top, recursive = True)
964 except git.GitException, ex:
965 print >> sys.stderr, \
966 'The merge failed during "push". ' \
967 'Use "refresh" after fixing the conflicts'
969 append_string(self.__applied_file, name)
971 unapplied.remove(name)
972 f = file(self.__unapplied_file, 'w+')
973 f.writelines([line + '\n' for line in unapplied])
976 self.__set_current(name)
978 # head == bottom case doesn't need to refresh the patch
979 if empty or head != bottom:
981 # if the merge was OK and no conflicts, just refresh the patch
982 # The GIT cache was already updated by the merge operation
987 self.refresh_patch(cache_update = False, log = log)
989 # we store the correctly merged files only for
990 # tracking the conflict history. Note that the
991 # git.merge() operations shouls always leave the index
992 # in a valid state (i.e. only stage 0 files)
993 self.refresh_patch(cache_update = False, log = 'push(c)')
994 raise StackException, str(ex)
999 name = self.get_current()
1002 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1003 old_bottom = patch.get_old_bottom()
1004 old_top = patch.get_old_top()
1006 # the top of the patch is changed by a push operation only
1007 # together with the bottom (otherwise the top was probably
1008 # modified by 'refresh'). If they are both unchanged, there
1009 # was a fast forward
1010 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1011 raise StackException, 'No undo information available'
1014 self.pop_patch(name)
1015 ret = patch.restore_old_boundaries()
1017 self.log_patch(patch, 'undo')
1021 def pop_patch(self, name, keep = False):
1022 """Pops the top patch from the stack
1024 applied = self.get_applied()
1026 assert(name in applied)
1028 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1030 # only keep the local changes
1031 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1032 raise StackException, \
1033 'Failed to pop patches while preserving the local changes'
1035 git.switch(patch.get_bottom(), keep)
1037 # save the new applied list
1038 idx = applied.index(name) + 1
1040 popped = applied[:idx]
1042 unapplied = popped + self.get_unapplied()
1044 f = file(self.__unapplied_file, 'w+')
1045 f.writelines([line + '\n' for line in unapplied])
1051 f = file(self.__applied_file, 'w+')
1052 f.writelines([line + '\n' for line in applied])
1056 self.__set_current(None)
1058 self.__set_current(applied[-1])
1060 self.__end_stack_check()
1062 def empty_patch(self, name):
1063 """Returns True if the patch is empty
1065 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1066 bottom = patch.get_bottom()
1067 top = patch.get_top()
1071 elif git.get_commit(top).get_tree() \
1072 == git.get_commit(bottom).get_tree():
1077 def rename_patch(self, oldname, newname):
1078 applied = self.get_applied()
1079 unapplied = self.get_unapplied()
1081 if oldname == newname:
1082 raise StackException, '"To" name and "from" name are the same'
1084 if newname in applied or newname in unapplied:
1085 raise StackException, 'Patch "%s" already exists' % newname
1087 if self.patch_hidden(oldname):
1088 self.unhide_patch(oldname)
1089 self.hide_patch(newname)
1091 if oldname in unapplied:
1092 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1093 unapplied[unapplied.index(oldname)] = newname
1095 f = file(self.__unapplied_file, 'w+')
1096 f.writelines([line + '\n' for line in unapplied])
1098 elif oldname in applied:
1099 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1100 if oldname == self.get_current():
1101 self.__set_current(newname)
1103 applied[applied.index(oldname)] = newname
1105 f = file(self.__applied_file, 'w+')
1106 f.writelines([line + '\n' for line in applied])
1109 raise StackException, 'Unknown patch "%s"' % oldname
1111 def log_patch(self, patch, message):
1112 """Generate a log commit for a patch
1114 top = git.get_commit(patch.get_top())
1115 msg = '%s\t%s' % (message, top.get_id_hash())
1117 old_log = patch.get_log()
1123 log = git.commit(message = msg, parents = parents,
1124 cache_update = False, tree_id = top.get_tree(),
1128 def hide_patch(self, name):
1129 """Add the patch to the hidden list.
1131 if not self.patch_exists(name):
1132 raise StackException, 'Unknown patch "%s"' % name
1133 elif self.patch_hidden(name):
1134 raise StackException, 'Patch "%s" already hidden' % name
1136 append_string(self.__hidden_file, name)
1138 def unhide_patch(self, name):
1139 """Add the patch to the hidden list.
1141 if not self.patch_exists(name):
1142 raise StackException, 'Unknown patch "%s"' % name
1143 hidden = self.get_hidden()
1144 if not name in hidden:
1145 raise StackException, 'Patch "%s" not hidden' % name
1149 f = file(self.__hidden_file, 'w+')
1150 f.writelines([line + '\n' for line in hidden])