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
398 return read_string(self.get_base_file())
401 """Return the head of the branch
403 crt = self.get_current_patch()
407 return self.get_base()
409 def get_protected(self):
410 return os.path.isfile(os.path.join(self._dir(), 'protected'))
413 protect_file = os.path.join(self._dir(), 'protected')
414 if not os.path.isfile(protect_file):
415 create_empty_file(protect_file)
418 protect_file = os.path.join(self._dir(), 'protected')
419 if os.path.isfile(protect_file):
420 os.remove(protect_file)
422 def get_description(self):
423 return self._get_field('description') or ''
425 def set_description(self, line):
426 self._set_field('description', line)
428 def get_parent_remote(self):
429 value = config.get('branch.%s.remote' % self.__name)
432 elif 'origin' in git.remotes_list():
433 print 'Notice: no parent remote declared for stack "%s", ' \
434 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
435 'and "branch.%s.merge" with "git repo-config".' \
436 % (self.__name, self.__name, self.__name)
439 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
441 def __set_parent_remote(self, remote):
442 value = config.set('branch.%s.remote' % self.__name, remote)
444 def get_parent_branch(self):
445 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
448 elif git.rev_parse('heads/origin'):
449 print 'Notice: no parent branch declared for stack "%s", ' \
450 'defaulting to "heads/origin". Consider setting ' \
451 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
452 % (self.__name, self.__name)
453 return 'heads/origin'
455 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
457 def __set_parent_branch(self, name):
458 if config.get('branch.%s.remote' % self.__name):
459 # Never set merge if remote is not set to avoid
460 # possibly-erroneous lookups into 'origin'
461 config.set('branch.%s.merge' % self.__name, name)
462 config.set('branch.%s.stgit.parentbranch' % self.__name, name)
464 def set_parent(self, remote, localbranch):
465 # policy: record local branches as remote='.'
466 recordremote = remote or '.'
468 self.__set_parent_remote(recordremote)
469 self.__set_parent_branch(localbranch)
470 # We'll enforce this later
472 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
474 def __patch_is_current(self, patch):
475 return patch.get_name() == self.get_current()
477 def patch_applied(self, name):
478 """Return true if the patch exists in the applied list
480 return name in self.get_applied()
482 def patch_unapplied(self, name):
483 """Return true if the patch exists in the unapplied list
485 return name in self.get_unapplied()
487 def patch_hidden(self, name):
488 """Return true if the patch is hidden.
490 return name in self.get_hidden()
492 def patch_exists(self, name):
493 """Return true if there is a patch with the given name, false
495 return self.patch_applied(name) or self.patch_unapplied(name)
497 def __begin_stack_check(self):
498 """Save the current HEAD into .git/refs/heads/base if the stack
501 if len(self.get_applied()) == 0:
502 head = git.get_head()
503 write_string(self.__base_file, head)
505 def __end_stack_check(self):
506 """Remove .git/refs/heads/base if the stack is empty.
507 This warning should never happen
509 if len(self.get_applied()) == 0 \
510 and read_string(self.__base_file) != git.get_head():
511 print 'Warning: stack empty but the HEAD and base are different'
513 def head_top_equal(self):
514 """Return true if the head and the top are the same
516 crt = self.get_current_patch()
518 # we don't care, no patches applied
520 return git.get_head() == crt.get_top()
522 def is_initialised(self):
523 """Checks if series is already initialised
525 return os.path.isdir(self.__patch_dir)
527 def init(self, create_at=False, parent_remote=None, parent_branch=None):
528 """Initialises the stgit series
530 if os.path.exists(self.__patch_dir):
531 raise StackException, self.__patch_dir + ' already exists'
532 if os.path.exists(self.__refs_dir):
533 raise StackException, self.__refs_dir + ' already exists'
534 if os.path.exists(self.__base_file):
535 raise StackException, self.__base_file + ' already exists'
537 if (create_at!=False):
538 git.create_branch(self.__name, create_at)
540 os.makedirs(self.__patch_dir)
542 self.set_parent(parent_remote, parent_branch)
544 create_dirs(os.path.join(self.__base_dir, 'refs', 'bases'))
546 self.create_empty_field('applied')
547 self.create_empty_field('unapplied')
548 self.create_empty_field('description')
549 os.makedirs(os.path.join(self._dir(), 'patches'))
550 os.makedirs(self.__refs_dir)
551 self.__begin_stack_check()
552 self._set_field('orig-base', git.get_head())
555 """Either convert to use a separate patch directory, or
556 unconvert to place the patches in the same directory with
559 if self.__patch_dir == self._dir():
560 print 'Converting old-style to new-style...',
563 self.__patch_dir = os.path.join(self._dir(), 'patches')
564 os.makedirs(self.__patch_dir)
566 for p in self.get_applied() + self.get_unapplied():
567 src = os.path.join(self._dir(), p)
568 dest = os.path.join(self.__patch_dir, p)
574 print 'Converting new-style to old-style...',
577 for p in self.get_applied() + self.get_unapplied():
578 src = os.path.join(self.__patch_dir, p)
579 dest = os.path.join(self._dir(), p)
582 if not os.listdir(self.__patch_dir):
583 os.rmdir(self.__patch_dir)
586 print 'Patch directory %s is not empty.' % self.__name
588 self.__patch_dir = self._dir()
590 def rename(self, to_name):
593 to_stack = Series(to_name)
595 if to_stack.is_initialised():
596 raise StackException, '"%s" already exists' % to_stack.get_branch()
597 if os.path.exists(to_stack.__base_file):
598 os.remove(to_stack.__base_file)
600 git.rename_branch(self.__name, to_name)
602 if os.path.isdir(self._dir()):
603 rename(os.path.join(self.__base_dir, 'patches'),
604 self.__name, to_stack.__name)
605 if os.path.exists(self.__base_file):
606 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
607 self.__name, to_stack.__name)
608 if os.path.exists(self.__refs_dir):
609 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
610 self.__name, to_stack.__name)
612 # Rename the config section
613 config.rename_section("branch.%s" % self.__name,
614 "branch.%s" % to_name)
616 self.__init__(to_name)
618 def clone(self, target_series):
622 # allow cloning of branches not under StGIT control
623 base = self.get_base()
625 base = git.get_head()
626 Series(target_series).init(create_at = base)
627 new_series = Series(target_series)
629 # generate an artificial description file
630 new_series.set_description('clone of "%s"' % self.__name)
632 # clone self's entire series as unapplied patches
634 # allow cloning of branches not under StGIT control
635 applied = self.get_applied()
636 unapplied = self.get_unapplied()
637 patches = applied + unapplied
640 patches = applied = unapplied = []
642 patch = self.get_patch(p)
643 new_series.new_patch(p, message = patch.get_description(),
644 can_edit = False, unapplied = True,
645 bottom = patch.get_bottom(),
646 top = patch.get_top(),
647 author_name = patch.get_authname(),
648 author_email = patch.get_authemail(),
649 author_date = patch.get_authdate())
651 # fast forward the cloned series to self's top
652 new_series.forward_patches(applied)
654 # Clone remote and merge settings
655 value = config.get('branch.%s.remote' % self.__name)
657 config.set('branch.%s.remote' % target_series, value)
659 value = config.get('branch.%s.merge' % self.__name)
661 config.set('branch.%s.merge' % target_series, value)
663 def delete(self, force = False):
664 """Deletes an stgit series
666 if self.is_initialised():
667 patches = self.get_unapplied() + self.get_applied()
668 if not force and patches:
669 raise StackException, \
670 'Cannot delete: the series still contains patches'
672 Patch(p, self.__patch_dir, self.__refs_dir).delete()
674 # remove the trash directory
675 for fname in os.listdir(self.__trash_dir):
677 os.rmdir(self.__trash_dir)
679 # FIXME: find a way to get rid of those manual removals
680 # (move functionality to StgitObject ?)
681 if os.path.exists(self.__applied_file):
682 os.remove(self.__applied_file)
683 if os.path.exists(self.__unapplied_file):
684 os.remove(self.__unapplied_file)
685 if os.path.exists(self.__hidden_file):
686 os.remove(self.__hidden_file)
687 if os.path.exists(self.__current_file):
688 os.remove(self.__current_file)
689 if os.path.exists(self.__descr_file):
690 os.remove(self.__descr_file)
691 if not os.listdir(self.__patch_dir):
692 os.rmdir(self.__patch_dir)
694 print 'Patch directory %s is not empty.' % self.__name
695 if not os.listdir(self._dir()):
696 remove_dirs(os.path.join(self.__base_dir, 'patches'),
699 print 'Series directory %s is not empty.' % self.__name
700 if not os.listdir(self.__refs_dir):
701 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
704 print 'Refs directory %s is not empty.' % self.__refs_dir
706 if os.path.exists(self.__base_file):
707 remove_file_and_dirs(
708 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
710 def refresh_patch(self, files = None, message = None, edit = False,
713 author_name = None, author_email = None,
715 committer_name = None, committer_email = None,
716 backup = False, sign_str = None, log = 'refresh'):
717 """Generates a new commit for the given patch
719 name = self.get_current()
721 raise StackException, 'No patches applied'
723 patch = Patch(name, self.__patch_dir, self.__refs_dir)
725 descr = patch.get_description()
726 if not (message or descr):
732 if not message and edit:
733 descr = edit_file(self, descr.rstrip(), \
734 'Please edit the description for patch "%s" ' \
735 'above.' % name, show_patch)
738 author_name = patch.get_authname()
740 author_email = patch.get_authemail()
742 author_date = patch.get_authdate()
743 if not committer_name:
744 committer_name = patch.get_commname()
745 if not committer_email:
746 committer_email = patch.get_commemail()
749 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
750 committer_name, committer_email)
752 bottom = patch.get_bottom()
754 commit_id = git.commit(files = files,
755 message = descr, parents = [bottom],
756 cache_update = cache_update,
758 author_name = author_name,
759 author_email = author_email,
760 author_date = author_date,
761 committer_name = committer_name,
762 committer_email = committer_email)
764 patch.set_bottom(bottom, backup = backup)
765 patch.set_top(commit_id, backup = backup)
766 patch.set_description(descr)
767 patch.set_authname(author_name)
768 patch.set_authemail(author_email)
769 patch.set_authdate(author_date)
770 patch.set_commname(committer_name)
771 patch.set_commemail(committer_email)
774 self.log_patch(patch, log)
778 def undo_refresh(self):
779 """Undo the patch boundaries changes caused by 'refresh'
781 name = self.get_current()
784 patch = Patch(name, self.__patch_dir, self.__refs_dir)
785 old_bottom = patch.get_old_bottom()
786 old_top = patch.get_old_top()
788 # the bottom of the patch is not changed by refresh. If the
789 # old_bottom is different, there wasn't any previous 'refresh'
790 # command (probably only a 'push')
791 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
792 raise StackException, 'No undo information available'
794 git.reset(tree_id = old_top, check_out = False)
795 if patch.restore_old_boundaries():
796 self.log_patch(patch, 'undo')
798 def new_patch(self, name, message = None, can_edit = True,
799 unapplied = False, show_patch = False,
800 top = None, bottom = None,
801 author_name = None, author_email = None, author_date = None,
802 committer_name = None, committer_email = None,
803 before_existing = False, refresh = True):
804 """Creates a new patch
806 self.__patch_name_valid(name)
808 if self.patch_applied(name) or self.patch_unapplied(name):
809 raise StackException, 'Patch "%s" already exists' % name
811 if not message and can_edit:
812 descr = edit_file(self, None, \
813 'Please enter the description for patch "%s" ' \
814 'above.' % name, show_patch)
818 head = git.get_head()
820 self.__begin_stack_check()
822 patch = Patch(name, self.__patch_dir, self.__refs_dir)
826 patch.set_bottom(bottom)
828 patch.set_bottom(head)
834 patch.set_description(descr)
835 patch.set_authname(author_name)
836 patch.set_authemail(author_email)
837 patch.set_authdate(author_date)
838 patch.set_commname(committer_name)
839 patch.set_commemail(committer_email)
842 self.log_patch(patch, 'new')
844 patches = [patch.get_name()] + self.get_unapplied()
846 f = file(self.__unapplied_file, 'w+')
847 f.writelines([line + '\n' for line in patches])
849 elif before_existing:
850 self.log_patch(patch, 'new')
852 insert_string(self.__applied_file, patch.get_name())
853 if not self.get_current():
854 self.__set_current(name)
856 append_string(self.__applied_file, patch.get_name())
857 self.__set_current(name)
859 self.refresh_patch(cache_update = False, log = 'new')
861 def delete_patch(self, name):
864 self.__patch_name_valid(name)
865 patch = Patch(name, self.__patch_dir, self.__refs_dir)
867 if self.__patch_is_current(patch):
869 elif self.patch_applied(name):
870 raise StackException, 'Cannot remove an applied patch, "%s", ' \
871 'which is not current' % name
872 elif not name in self.get_unapplied():
873 raise StackException, 'Unknown patch "%s"' % name
875 # save the commit id to a trash file
876 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
880 unapplied = self.get_unapplied()
881 unapplied.remove(name)
882 f = file(self.__unapplied_file, 'w+')
883 f.writelines([line + '\n' for line in unapplied])
886 if self.patch_hidden(name):
887 self.unhide_patch(name)
889 self.__begin_stack_check()
891 def forward_patches(self, names):
892 """Try to fast-forward an array of patches.
894 On return, patches in names[0:returned_value] have been pushed on the
895 stack. Apply the rest with push_patch
897 unapplied = self.get_unapplied()
898 self.__begin_stack_check()
904 assert(name in unapplied)
906 patch = Patch(name, self.__patch_dir, self.__refs_dir)
909 bottom = patch.get_bottom()
910 top = patch.get_top()
912 # top != bottom always since we have a commit for each patch
914 # reset the backup information. No logging since the
915 # patch hasn't changed
916 patch.set_bottom(head, backup = True)
917 patch.set_top(top, backup = True)
920 head_tree = git.get_commit(head).get_tree()
921 bottom_tree = git.get_commit(bottom).get_tree()
922 if head_tree == bottom_tree:
923 # We must just reparent this patch and create a new commit
925 descr = patch.get_description()
926 author_name = patch.get_authname()
927 author_email = patch.get_authemail()
928 author_date = patch.get_authdate()
929 committer_name = patch.get_commname()
930 committer_email = patch.get_commemail()
932 top_tree = git.get_commit(top).get_tree()
934 top = git.commit(message = descr, parents = [head],
935 cache_update = False,
938 author_name = author_name,
939 author_email = author_email,
940 author_date = author_date,
941 committer_name = committer_name,
942 committer_email = committer_email)
944 patch.set_bottom(head, backup = True)
945 patch.set_top(top, backup = True)
947 self.log_patch(patch, 'push(f)')
950 # stop the fast-forwarding, must do a real merge
954 unapplied.remove(name)
961 append_strings(self.__applied_file, names[0:forwarded])
963 f = file(self.__unapplied_file, 'w+')
964 f.writelines([line + '\n' for line in unapplied])
967 self.__set_current(name)
971 def merged_patches(self, names):
972 """Test which patches were merged upstream by reverse-applying
973 them in reverse order. The function returns the list of
974 patches detected to have been applied. The state of the tree
975 is restored to the original one
977 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
983 if git.apply_diff(p.get_top(), p.get_bottom()):
984 merged.append(p.get_name())
991 def push_patch(self, name, empty = False):
992 """Pushes a patch on the stack
994 unapplied = self.get_unapplied()
995 assert(name in unapplied)
997 self.__begin_stack_check()
999 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1001 head = git.get_head()
1002 bottom = patch.get_bottom()
1003 top = patch.get_top()
1008 # top != bottom always since we have a commit for each patch
1010 # just make an empty patch (top = bottom = HEAD). This
1011 # option is useful to allow undoing already merged
1012 # patches. The top is updated by refresh_patch since we
1013 # need an empty commit
1014 patch.set_bottom(head, backup = True)
1015 patch.set_top(head, backup = True)
1017 elif head == bottom:
1018 # reset the backup information. No need for logging
1019 patch.set_bottom(bottom, backup = True)
1020 patch.set_top(top, backup = True)
1024 # new patch needs to be refreshed.
1025 # The current patch is empty after merge.
1026 patch.set_bottom(head, backup = True)
1027 patch.set_top(head, backup = True)
1029 # Try the fast applying first. If this fails, fall back to the
1031 if not git.apply_diff(bottom, top):
1032 # if git.apply_diff() fails, the patch requires a diff3
1033 # merge and can be reported as modified
1036 # merge can fail but the patch needs to be pushed
1038 git.merge(bottom, head, top, recursive = True)
1039 except git.GitException, ex:
1040 print >> sys.stderr, \
1041 'The merge failed during "push". ' \
1042 'Use "refresh" after fixing the conflicts'
1044 append_string(self.__applied_file, name)
1046 unapplied.remove(name)
1047 f = file(self.__unapplied_file, 'w+')
1048 f.writelines([line + '\n' for line in unapplied])
1051 self.__set_current(name)
1053 # head == bottom case doesn't need to refresh the patch
1054 if empty or head != bottom:
1056 # if the merge was OK and no conflicts, just refresh the patch
1057 # The GIT cache was already updated by the merge operation
1062 self.refresh_patch(cache_update = False, log = log)
1064 # we store the correctly merged files only for
1065 # tracking the conflict history. Note that the
1066 # git.merge() operations should always leave the index
1067 # in a valid state (i.e. only stage 0 files)
1068 self.refresh_patch(cache_update = False, log = 'push(c)')
1069 raise StackException, str(ex)
1073 def undo_push(self):
1074 name = self.get_current()
1077 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1078 old_bottom = patch.get_old_bottom()
1079 old_top = patch.get_old_top()
1081 # the top of the patch is changed by a push operation only
1082 # together with the bottom (otherwise the top was probably
1083 # modified by 'refresh'). If they are both unchanged, there
1084 # was a fast forward
1085 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1086 raise StackException, 'No undo information available'
1089 self.pop_patch(name)
1090 ret = patch.restore_old_boundaries()
1092 self.log_patch(patch, 'undo')
1096 def pop_patch(self, name, keep = False):
1097 """Pops the top patch from the stack
1099 applied = self.get_applied()
1101 assert(name in applied)
1103 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1105 # only keep the local changes
1106 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1107 raise StackException, \
1108 'Failed to pop patches while preserving the local changes'
1110 git.switch(patch.get_bottom(), keep)
1112 # save the new applied list
1113 idx = applied.index(name) + 1
1115 popped = applied[:idx]
1117 unapplied = popped + self.get_unapplied()
1119 f = file(self.__unapplied_file, 'w+')
1120 f.writelines([line + '\n' for line in unapplied])
1126 f = file(self.__applied_file, 'w+')
1127 f.writelines([line + '\n' for line in applied])
1131 self.__set_current(None)
1133 self.__set_current(applied[-1])
1135 self.__end_stack_check()
1137 def empty_patch(self, name):
1138 """Returns True if the patch is empty
1140 self.__patch_name_valid(name)
1141 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1142 bottom = patch.get_bottom()
1143 top = patch.get_top()
1147 elif git.get_commit(top).get_tree() \
1148 == git.get_commit(bottom).get_tree():
1153 def rename_patch(self, oldname, newname):
1154 self.__patch_name_valid(newname)
1156 applied = self.get_applied()
1157 unapplied = self.get_unapplied()
1159 if oldname == newname:
1160 raise StackException, '"To" name and "from" name are the same'
1162 if newname in applied or newname in unapplied:
1163 raise StackException, 'Patch "%s" already exists' % newname
1165 if self.patch_hidden(oldname):
1166 self.unhide_patch(oldname)
1167 self.hide_patch(newname)
1169 if oldname in unapplied:
1170 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1171 unapplied[unapplied.index(oldname)] = newname
1173 f = file(self.__unapplied_file, 'w+')
1174 f.writelines([line + '\n' for line in unapplied])
1176 elif oldname in applied:
1177 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1178 if oldname == self.get_current():
1179 self.__set_current(newname)
1181 applied[applied.index(oldname)] = newname
1183 f = file(self.__applied_file, 'w+')
1184 f.writelines([line + '\n' for line in applied])
1187 raise StackException, 'Unknown patch "%s"' % oldname
1189 def log_patch(self, patch, message):
1190 """Generate a log commit for a patch
1192 top = git.get_commit(patch.get_top())
1193 msg = '%s\t%s' % (message, top.get_id_hash())
1195 old_log = patch.get_log()
1201 log = git.commit(message = msg, parents = parents,
1202 cache_update = False, tree_id = top.get_tree(),
1206 def hide_patch(self, name):
1207 """Add the patch to the hidden list.
1209 if not self.patch_exists(name):
1210 raise StackException, 'Unknown patch "%s"' % name
1211 elif self.patch_hidden(name):
1212 raise StackException, 'Patch "%s" already hidden' % name
1214 append_string(self.__hidden_file, name)
1216 def unhide_patch(self, name):
1217 """Add the patch to the hidden list.
1219 if not self.patch_exists(name):
1220 raise StackException, 'Unknown patch "%s"' % name
1221 hidden = self.get_hidden()
1222 if not name in hidden:
1223 raise StackException, 'Patch "%s" not hidden' % name
1227 f = file(self.__hidden_file, 'w+')
1228 f.writelines([line + '\n' for line in hidden])