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())
400 def get_protected(self):
401 return os.path.isfile(os.path.join(self._dir(), 'protected'))
404 protect_file = os.path.join(self._dir(), 'protected')
405 if not os.path.isfile(protect_file):
406 create_empty_file(protect_file)
409 protect_file = os.path.join(self._dir(), 'protected')
410 if os.path.isfile(protect_file):
411 os.remove(protect_file)
413 def get_description(self):
414 return self._get_field('description') or ''
416 def set_description(self, line):
417 self._set_field('description', line)
419 def get_parent_remote(self):
420 value = config.get('branch.%s.remote' % self.__name)
423 elif 'origin' in git.remotes_list():
424 print 'Notice: no parent remote declared for stack "%s", ' \
425 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
426 'and "branch.%s.merge" with "git repo-config".' \
427 % (self.__name, self.__name, self.__name)
430 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
432 def __set_parent_remote(self, remote):
433 value = config.set('branch.%s.remote' % self.__name, remote)
435 def get_parent_branch(self):
436 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
439 elif git.rev_parse('heads/origin'):
440 print 'Notice: no parent branch declared for stack "%s", ' \
441 'defaulting to "heads/origin". Consider setting ' \
442 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
443 % (self.__name, self.__name)
444 return 'heads/origin'
446 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
448 def __set_parent_branch(self, name):
449 if config.get('branch.%s.remote' % self.__name):
450 # Never set merge if remote is not set to avoid
451 # possibly-erroneous lookups into 'origin'
452 config.set('branch.%s.merge' % self.__name, name)
453 config.set('branch.%s.stgit.parentbranch' % self.__name, name)
455 def set_parent(self, remote, localbranch):
456 # policy: record local branches as remote='.'
457 recordremote = remote or '.'
459 self.__set_parent_remote(recordremote)
460 self.__set_parent_branch(localbranch)
461 # We'll enforce this later
463 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
465 def __patch_is_current(self, patch):
466 return patch.get_name() == self.get_current()
468 def patch_applied(self, name):
469 """Return true if the patch exists in the applied list
471 return name in self.get_applied()
473 def patch_unapplied(self, name):
474 """Return true if the patch exists in the unapplied list
476 return name in self.get_unapplied()
478 def patch_hidden(self, name):
479 """Return true if the patch is hidden.
481 return name in self.get_hidden()
483 def patch_exists(self, name):
484 """Return true if there is a patch with the given name, false
486 return self.patch_applied(name) or self.patch_unapplied(name)
488 def __begin_stack_check(self):
489 """Save the current HEAD into .git/refs/heads/base if the stack
492 if len(self.get_applied()) == 0:
493 head = git.get_head()
494 write_string(self.__base_file, head)
496 def __end_stack_check(self):
497 """Remove .git/refs/heads/base if the stack is empty.
498 This warning should never happen
500 if len(self.get_applied()) == 0 \
501 and read_string(self.__base_file) != git.get_head():
502 print 'Warning: stack empty but the HEAD and base are different'
504 def head_top_equal(self):
505 """Return true if the head and the top are the same
507 crt = self.get_current_patch()
509 # we don't care, no patches applied
511 return git.get_head() == crt.get_top()
513 def is_initialised(self):
514 """Checks if series is already initialised
516 return os.path.isdir(self.__patch_dir)
518 def init(self, create_at=False, parent_remote=None, parent_branch=None):
519 """Initialises the stgit series
521 if os.path.exists(self.__patch_dir):
522 raise StackException, self.__patch_dir + ' already exists'
523 if os.path.exists(self.__refs_dir):
524 raise StackException, self.__refs_dir + ' already exists'
525 if os.path.exists(self.__base_file):
526 raise StackException, self.__base_file + ' already exists'
528 if (create_at!=False):
529 git.create_branch(self.__name, create_at)
531 os.makedirs(self.__patch_dir)
533 self.set_parent(parent_remote, parent_branch)
535 create_dirs(os.path.join(self.__base_dir, 'refs', 'bases'))
537 self.create_empty_field('applied')
538 self.create_empty_field('unapplied')
539 self.create_empty_field('description')
540 os.makedirs(os.path.join(self._dir(), 'patches'))
541 os.makedirs(self.__refs_dir)
542 self.__begin_stack_check()
543 self._set_field('orig-base', git.get_head())
546 """Either convert to use a separate patch directory, or
547 unconvert to place the patches in the same directory with
550 if self.__patch_dir == self._dir():
551 print 'Converting old-style to new-style...',
554 self.__patch_dir = os.path.join(self._dir(), 'patches')
555 os.makedirs(self.__patch_dir)
557 for p in self.get_applied() + self.get_unapplied():
558 src = os.path.join(self._dir(), p)
559 dest = os.path.join(self.__patch_dir, p)
565 print 'Converting new-style to old-style...',
568 for p in self.get_applied() + self.get_unapplied():
569 src = os.path.join(self.__patch_dir, p)
570 dest = os.path.join(self._dir(), p)
573 if not os.listdir(self.__patch_dir):
574 os.rmdir(self.__patch_dir)
577 print 'Patch directory %s is not empty.' % self.__name
579 self.__patch_dir = self._dir()
581 def rename(self, to_name):
584 to_stack = Series(to_name)
586 if to_stack.is_initialised():
587 raise StackException, '"%s" already exists' % to_stack.get_branch()
588 if os.path.exists(to_stack.__base_file):
589 os.remove(to_stack.__base_file)
591 git.rename_branch(self.__name, to_name)
593 if os.path.isdir(self._dir()):
594 rename(os.path.join(self.__base_dir, 'patches'),
595 self.__name, to_stack.__name)
596 if os.path.exists(self.__base_file):
597 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
598 self.__name, to_stack.__name)
599 if os.path.exists(self.__refs_dir):
600 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
601 self.__name, to_stack.__name)
603 # Rename the config section
604 config.rename_section("branch.%s" % self.__name,
605 "branch.%s" % to_name)
607 self.__init__(to_name)
609 def clone(self, target_series):
613 # allow cloning of branches not under StGIT control
614 base = self.get_base()
616 base = git.get_head()
617 Series(target_series).init(create_at = base)
618 new_series = Series(target_series)
620 # generate an artificial description file
621 new_series.set_description('clone of "%s"' % self.__name)
623 # clone self's entire series as unapplied patches
625 # allow cloning of branches not under StGIT control
626 applied = self.get_applied()
627 unapplied = self.get_unapplied()
628 patches = applied + unapplied
631 patches = applied = unapplied = []
633 patch = self.get_patch(p)
634 new_series.new_patch(p, message = patch.get_description(),
635 can_edit = False, unapplied = True,
636 bottom = patch.get_bottom(),
637 top = patch.get_top(),
638 author_name = patch.get_authname(),
639 author_email = patch.get_authemail(),
640 author_date = patch.get_authdate())
642 # fast forward the cloned series to self's top
643 new_series.forward_patches(applied)
645 # Clone remote and merge settings
646 value = config.get('branch.%s.remote' % self.__name)
648 config.set('branch.%s.remote' % target_series, value)
650 value = config.get('branch.%s.merge' % self.__name)
652 config.set('branch.%s.merge' % target_series, value)
654 def delete(self, force = False):
655 """Deletes an stgit series
657 if self.is_initialised():
658 patches = self.get_unapplied() + self.get_applied()
659 if not force and patches:
660 raise StackException, \
661 'Cannot delete: the series still contains patches'
663 Patch(p, self.__patch_dir, self.__refs_dir).delete()
665 # remove the trash directory
666 for fname in os.listdir(self.__trash_dir):
668 os.rmdir(self.__trash_dir)
670 # FIXME: find a way to get rid of those manual removals
671 # (move functionality to StgitObject ?)
672 if os.path.exists(self.__applied_file):
673 os.remove(self.__applied_file)
674 if os.path.exists(self.__unapplied_file):
675 os.remove(self.__unapplied_file)
676 if os.path.exists(self.__hidden_file):
677 os.remove(self.__hidden_file)
678 if os.path.exists(self.__current_file):
679 os.remove(self.__current_file)
680 if os.path.exists(self.__descr_file):
681 os.remove(self.__descr_file)
682 if not os.listdir(self.__patch_dir):
683 os.rmdir(self.__patch_dir)
685 print 'Patch directory %s is not empty.' % self.__name
686 if not os.listdir(self._dir()):
687 remove_dirs(os.path.join(self.__base_dir, 'patches'),
690 print 'Series directory %s is not empty.' % self.__name
691 if not os.listdir(self.__refs_dir):
692 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
695 print 'Refs directory %s is not empty.' % self.__refs_dir
697 if os.path.exists(self.__base_file):
698 remove_file_and_dirs(
699 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
701 def refresh_patch(self, files = None, message = None, edit = False,
704 author_name = None, author_email = None,
706 committer_name = None, committer_email = None,
707 backup = False, sign_str = None, log = 'refresh'):
708 """Generates a new commit for the given patch
710 name = self.get_current()
712 raise StackException, 'No patches applied'
714 patch = Patch(name, self.__patch_dir, self.__refs_dir)
716 descr = patch.get_description()
717 if not (message or descr):
723 if not message and edit:
724 descr = edit_file(self, descr.rstrip(), \
725 'Please edit the description for patch "%s" ' \
726 'above.' % name, show_patch)
729 author_name = patch.get_authname()
731 author_email = patch.get_authemail()
733 author_date = patch.get_authdate()
734 if not committer_name:
735 committer_name = patch.get_commname()
736 if not committer_email:
737 committer_email = patch.get_commemail()
740 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
741 committer_name, committer_email)
743 bottom = patch.get_bottom()
745 commit_id = git.commit(files = files,
746 message = descr, parents = [bottom],
747 cache_update = cache_update,
749 author_name = author_name,
750 author_email = author_email,
751 author_date = author_date,
752 committer_name = committer_name,
753 committer_email = committer_email)
755 patch.set_bottom(bottom, backup = backup)
756 patch.set_top(commit_id, backup = backup)
757 patch.set_description(descr)
758 patch.set_authname(author_name)
759 patch.set_authemail(author_email)
760 patch.set_authdate(author_date)
761 patch.set_commname(committer_name)
762 patch.set_commemail(committer_email)
765 self.log_patch(patch, log)
769 def undo_refresh(self):
770 """Undo the patch boundaries changes caused by 'refresh'
772 name = self.get_current()
775 patch = Patch(name, self.__patch_dir, self.__refs_dir)
776 old_bottom = patch.get_old_bottom()
777 old_top = patch.get_old_top()
779 # the bottom of the patch is not changed by refresh. If the
780 # old_bottom is different, there wasn't any previous 'refresh'
781 # command (probably only a 'push')
782 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
783 raise StackException, 'No undo information available'
785 git.reset(tree_id = old_top, check_out = False)
786 if patch.restore_old_boundaries():
787 self.log_patch(patch, 'undo')
789 def new_patch(self, name, message = None, can_edit = True,
790 unapplied = False, show_patch = False,
791 top = None, bottom = None,
792 author_name = None, author_email = None, author_date = None,
793 committer_name = None, committer_email = None,
794 before_existing = False, refresh = True):
795 """Creates a new patch
797 self.__patch_name_valid(name)
799 if self.patch_applied(name) or self.patch_unapplied(name):
800 raise StackException, 'Patch "%s" already exists' % name
802 if not message and can_edit:
803 descr = edit_file(self, None, \
804 'Please enter the description for patch "%s" ' \
805 'above.' % name, show_patch)
809 head = git.get_head()
811 self.__begin_stack_check()
813 patch = Patch(name, self.__patch_dir, self.__refs_dir)
817 patch.set_bottom(bottom)
819 patch.set_bottom(head)
825 patch.set_description(descr)
826 patch.set_authname(author_name)
827 patch.set_authemail(author_email)
828 patch.set_authdate(author_date)
829 patch.set_commname(committer_name)
830 patch.set_commemail(committer_email)
833 self.log_patch(patch, 'new')
835 patches = [patch.get_name()] + self.get_unapplied()
837 f = file(self.__unapplied_file, 'w+')
838 f.writelines([line + '\n' for line in patches])
840 elif before_existing:
841 self.log_patch(patch, 'new')
843 insert_string(self.__applied_file, patch.get_name())
844 if not self.get_current():
845 self.__set_current(name)
847 append_string(self.__applied_file, patch.get_name())
848 self.__set_current(name)
850 self.refresh_patch(cache_update = False, log = 'new')
852 def delete_patch(self, name):
855 self.__patch_name_valid(name)
856 patch = Patch(name, self.__patch_dir, self.__refs_dir)
858 if self.__patch_is_current(patch):
860 elif self.patch_applied(name):
861 raise StackException, 'Cannot remove an applied patch, "%s", ' \
862 'which is not current' % name
863 elif not name in self.get_unapplied():
864 raise StackException, 'Unknown patch "%s"' % name
866 # save the commit id to a trash file
867 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
871 unapplied = self.get_unapplied()
872 unapplied.remove(name)
873 f = file(self.__unapplied_file, 'w+')
874 f.writelines([line + '\n' for line in unapplied])
877 if self.patch_hidden(name):
878 self.unhide_patch(name)
880 self.__begin_stack_check()
882 def forward_patches(self, names):
883 """Try to fast-forward an array of patches.
885 On return, patches in names[0:returned_value] have been pushed on the
886 stack. Apply the rest with push_patch
888 unapplied = self.get_unapplied()
889 self.__begin_stack_check()
895 assert(name in unapplied)
897 patch = Patch(name, self.__patch_dir, self.__refs_dir)
900 bottom = patch.get_bottom()
901 top = patch.get_top()
903 # top != bottom always since we have a commit for each patch
905 # reset the backup information. No logging since the
906 # patch hasn't changed
907 patch.set_bottom(head, backup = True)
908 patch.set_top(top, backup = True)
911 head_tree = git.get_commit(head).get_tree()
912 bottom_tree = git.get_commit(bottom).get_tree()
913 if head_tree == bottom_tree:
914 # We must just reparent this patch and create a new commit
916 descr = patch.get_description()
917 author_name = patch.get_authname()
918 author_email = patch.get_authemail()
919 author_date = patch.get_authdate()
920 committer_name = patch.get_commname()
921 committer_email = patch.get_commemail()
923 top_tree = git.get_commit(top).get_tree()
925 top = git.commit(message = descr, parents = [head],
926 cache_update = False,
929 author_name = author_name,
930 author_email = author_email,
931 author_date = author_date,
932 committer_name = committer_name,
933 committer_email = committer_email)
935 patch.set_bottom(head, backup = True)
936 patch.set_top(top, backup = True)
938 self.log_patch(patch, 'push(f)')
941 # stop the fast-forwarding, must do a real merge
945 unapplied.remove(name)
952 append_strings(self.__applied_file, names[0:forwarded])
954 f = file(self.__unapplied_file, 'w+')
955 f.writelines([line + '\n' for line in unapplied])
958 self.__set_current(name)
962 def merged_patches(self, names):
963 """Test which patches were merged upstream by reverse-applying
964 them in reverse order. The function returns the list of
965 patches detected to have been applied. The state of the tree
966 is restored to the original one
968 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
974 if git.apply_diff(p.get_top(), p.get_bottom()):
975 merged.append(p.get_name())
982 def push_patch(self, name, empty = False):
983 """Pushes a patch on the stack
985 unapplied = self.get_unapplied()
986 assert(name in unapplied)
988 self.__begin_stack_check()
990 patch = Patch(name, self.__patch_dir, self.__refs_dir)
992 head = git.get_head()
993 bottom = patch.get_bottom()
994 top = patch.get_top()
999 # top != bottom always since we have a commit for each patch
1001 # just make an empty patch (top = bottom = HEAD). This
1002 # option is useful to allow undoing already merged
1003 # patches. The top is updated by refresh_patch since we
1004 # need an empty commit
1005 patch.set_bottom(head, backup = True)
1006 patch.set_top(head, backup = True)
1008 elif head == bottom:
1009 # reset the backup information. No need for logging
1010 patch.set_bottom(bottom, backup = True)
1011 patch.set_top(top, backup = True)
1015 # new patch needs to be refreshed.
1016 # The current patch is empty after merge.
1017 patch.set_bottom(head, backup = True)
1018 patch.set_top(head, backup = True)
1020 # Try the fast applying first. If this fails, fall back to the
1022 if not git.apply_diff(bottom, top):
1023 # if git.apply_diff() fails, the patch requires a diff3
1024 # merge and can be reported as modified
1027 # merge can fail but the patch needs to be pushed
1029 git.merge(bottom, head, top, recursive = True)
1030 except git.GitException, ex:
1031 print >> sys.stderr, \
1032 'The merge failed during "push". ' \
1033 'Use "refresh" after fixing the conflicts'
1035 append_string(self.__applied_file, name)
1037 unapplied.remove(name)
1038 f = file(self.__unapplied_file, 'w+')
1039 f.writelines([line + '\n' for line in unapplied])
1042 self.__set_current(name)
1044 # head == bottom case doesn't need to refresh the patch
1045 if empty or head != bottom:
1047 # if the merge was OK and no conflicts, just refresh the patch
1048 # The GIT cache was already updated by the merge operation
1053 self.refresh_patch(cache_update = False, log = log)
1055 # we store the correctly merged files only for
1056 # tracking the conflict history. Note that the
1057 # git.merge() operations should always leave the index
1058 # in a valid state (i.e. only stage 0 files)
1059 self.refresh_patch(cache_update = False, log = 'push(c)')
1060 raise StackException, str(ex)
1064 def undo_push(self):
1065 name = self.get_current()
1068 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1069 old_bottom = patch.get_old_bottom()
1070 old_top = patch.get_old_top()
1072 # the top of the patch is changed by a push operation only
1073 # together with the bottom (otherwise the top was probably
1074 # modified by 'refresh'). If they are both unchanged, there
1075 # was a fast forward
1076 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1077 raise StackException, 'No undo information available'
1080 self.pop_patch(name)
1081 ret = patch.restore_old_boundaries()
1083 self.log_patch(patch, 'undo')
1087 def pop_patch(self, name, keep = False):
1088 """Pops the top patch from the stack
1090 applied = self.get_applied()
1092 assert(name in applied)
1094 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1096 # only keep the local changes
1097 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1098 raise StackException, \
1099 'Failed to pop patches while preserving the local changes'
1101 git.switch(patch.get_bottom(), keep)
1103 # save the new applied list
1104 idx = applied.index(name) + 1
1106 popped = applied[:idx]
1108 unapplied = popped + self.get_unapplied()
1110 f = file(self.__unapplied_file, 'w+')
1111 f.writelines([line + '\n' for line in unapplied])
1117 f = file(self.__applied_file, 'w+')
1118 f.writelines([line + '\n' for line in applied])
1122 self.__set_current(None)
1124 self.__set_current(applied[-1])
1126 self.__end_stack_check()
1128 def empty_patch(self, name):
1129 """Returns True if the patch is empty
1131 self.__patch_name_valid(name)
1132 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1133 bottom = patch.get_bottom()
1134 top = patch.get_top()
1138 elif git.get_commit(top).get_tree() \
1139 == git.get_commit(bottom).get_tree():
1144 def rename_patch(self, oldname, newname):
1145 self.__patch_name_valid(newname)
1147 applied = self.get_applied()
1148 unapplied = self.get_unapplied()
1150 if oldname == newname:
1151 raise StackException, '"To" name and "from" name are the same'
1153 if newname in applied or newname in unapplied:
1154 raise StackException, 'Patch "%s" already exists' % newname
1156 if self.patch_hidden(oldname):
1157 self.unhide_patch(oldname)
1158 self.hide_patch(newname)
1160 if oldname in unapplied:
1161 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1162 unapplied[unapplied.index(oldname)] = newname
1164 f = file(self.__unapplied_file, 'w+')
1165 f.writelines([line + '\n' for line in unapplied])
1167 elif oldname in applied:
1168 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1169 if oldname == self.get_current():
1170 self.__set_current(newname)
1172 applied[applied.index(oldname)] = newname
1174 f = file(self.__applied_file, 'w+')
1175 f.writelines([line + '\n' for line in applied])
1178 raise StackException, 'Unknown patch "%s"' % oldname
1180 def log_patch(self, patch, message):
1181 """Generate a log commit for a patch
1183 top = git.get_commit(patch.get_top())
1184 msg = '%s\t%s' % (message, top.get_id_hash())
1186 old_log = patch.get_log()
1192 log = git.commit(message = msg, parents = parents,
1193 cache_update = False, tree_id = top.get_tree(),
1197 def hide_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 elif self.patch_hidden(name):
1203 raise StackException, 'Patch "%s" already hidden' % name
1205 append_string(self.__hidden_file, name)
1207 def unhide_patch(self, name):
1208 """Add the patch to the hidden list.
1210 if not self.patch_exists(name):
1211 raise StackException, 'Unknown patch "%s"' % name
1212 hidden = self.get_hidden()
1213 if not name in hidden:
1214 raise StackException, 'Patch "%s" not hidden' % name
1218 f = file(self.__hidden_file, 'w+')
1219 f.writelines([line + '\n' for line in hidden])