1 """Basic quilt-like functionality
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 from stgit.utils import *
24 from stgit import git, basedir, templates
25 from stgit.config import config
28 # stack exception class
29 class StackException(Exception):
34 self.should_print = True
35 def __call__(self, x, until_test, prefix):
37 self.should_print = False
39 return x[0:len(prefix)] != prefix
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
48 def __clean_comments(f):
49 """Removes lines marked for status in a commit file
53 # remove status-prefixed lines
56 patch_filter = FilterUntil()
57 until_test = lambda t: t == (__patch_prefix + '\n')
58 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
60 # remove empty lines at the end
61 while len(lines) != 0 and lines[-1] == '\n':
64 f.seek(0); f.truncate()
67 def edit_file(series, line, comment, show_patch = True):
68 fname = '.stgitmsg.txt'
69 tmpl = templates.get_template('patchdescr.tmpl')
78 print >> f, __comment_prefix, comment
79 print >> f, __comment_prefix, \
80 'Lines prefixed with "%s" will be automatically removed.' \
82 print >> f, __comment_prefix, \
83 'Trailing empty lines will be automatically removed.'
86 print >> f, __patch_prefix
87 # series.get_patch(series.get_current()).get_top()
88 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
90 #Vim modeline must be near the end.
91 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
95 editor = config.get('stgit.editor')
98 elif 'EDITOR' in os.environ:
99 editor = os.environ['EDITOR']
102 editor += ' %s' % fname
104 print 'Invoking the editor: "%s"...' % editor,
106 print 'done (exit code: %d)' % os.system(editor)
108 f = file(fname, 'r+')
124 """An object with stgit-like properties stored as files in a directory
126 def _set_dir(self, dir):
131 def create_empty_field(self, name):
132 create_empty_file(os.path.join(self.__dir, name))
134 def _get_field(self, name, multiline = False):
135 id_file = os.path.join(self.__dir, name)
136 if os.path.isfile(id_file):
137 line = read_string(id_file, multiline)
145 def _set_field(self, name, value, multiline = False):
146 fname = os.path.join(self.__dir, name)
147 if value and value != '':
148 write_string(fname, value, multiline)
149 elif os.path.isfile(fname):
153 class Patch(StgitObject):
154 """Basic patch implementation
156 def __init__(self, name, series_dir, refs_dir):
157 self.__series_dir = series_dir
159 self._set_dir(os.path.join(self.__series_dir, self.__name))
160 self.__refs_dir = refs_dir
161 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
162 self.__log_ref_file = os.path.join(self.__refs_dir,
163 self.__name + '.log')
166 os.mkdir(self._dir())
167 self.create_empty_field('bottom')
168 self.create_empty_field('top')
171 for f in os.listdir(self._dir()):
172 os.remove(os.path.join(self._dir(), f))
173 os.rmdir(self._dir())
174 os.remove(self.__top_ref_file)
175 if os.path.exists(self.__log_ref_file):
176 os.remove(self.__log_ref_file)
181 def rename(self, newname):
183 old_top_ref_file = self.__top_ref_file
184 old_log_ref_file = self.__log_ref_file
185 self.__name = newname
186 self._set_dir(os.path.join(self.__series_dir, self.__name))
187 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
188 self.__log_ref_file = os.path.join(self.__refs_dir,
189 self.__name + '.log')
191 os.rename(olddir, self._dir())
192 os.rename(old_top_ref_file, self.__top_ref_file)
193 if os.path.exists(old_log_ref_file):
194 os.rename(old_log_ref_file, self.__log_ref_file)
196 def __update_top_ref(self, ref):
197 write_string(self.__top_ref_file, ref)
199 def __update_log_ref(self, ref):
200 write_string(self.__log_ref_file, ref)
202 def update_top_ref(self):
205 self.__update_top_ref(top)
207 def get_old_bottom(self):
208 return self._get_field('bottom.old')
210 def get_bottom(self):
211 return self._get_field('bottom')
213 def set_bottom(self, value, backup = False):
215 curr = self._get_field('bottom')
216 self._set_field('bottom.old', curr)
217 self._set_field('bottom', value)
219 def get_old_top(self):
220 return self._get_field('top.old')
223 return self._get_field('top')
225 def set_top(self, value, backup = False):
227 curr = self._get_field('top')
228 self._set_field('top.old', curr)
229 self._set_field('top', value)
230 self.__update_top_ref(value)
232 def restore_old_boundaries(self):
233 bottom = self._get_field('bottom.old')
234 top = self._get_field('top.old')
237 self._set_field('bottom', bottom)
238 self._set_field('top', top)
239 self.__update_top_ref(top)
244 def get_description(self):
245 return self._get_field('description', True)
247 def set_description(self, line):
248 self._set_field('description', line, True)
250 def get_authname(self):
251 return self._get_field('authname')
253 def set_authname(self, name):
254 self._set_field('authname', name or git.author().name)
256 def get_authemail(self):
257 return self._get_field('authemail')
259 def set_authemail(self, email):
260 self._set_field('authemail', email or git.author().email)
262 def get_authdate(self):
263 return self._get_field('authdate')
265 def set_authdate(self, date):
266 self._set_field('authdate', date or git.author().date)
268 def get_commname(self):
269 return self._get_field('commname')
271 def set_commname(self, name):
272 self._set_field('commname', name or git.committer().name)
274 def get_commemail(self):
275 return self._get_field('commemail')
277 def set_commemail(self, email):
278 self._set_field('commemail', email or git.committer().email)
281 return self._get_field('log')
283 def set_log(self, value, backup = False):
284 self._set_field('log', value)
285 self.__update_log_ref(value)
288 class Series(StgitObject):
289 """Class including the operations on series
291 def __init__(self, name = None):
292 """Takes a series name as the parameter.
298 self.__name = git.get_head_file()
299 self.__base_dir = basedir.get()
300 except git.GitException, ex:
301 raise StackException, 'GIT tree not initialised: %s' % ex
303 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
304 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
306 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
309 self.__applied_file = os.path.join(self._dir(), 'applied')
310 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
311 self.__hidden_file = os.path.join(self._dir(), 'hidden')
312 self.__current_file = os.path.join(self._dir(), 'current')
313 self.__descr_file = os.path.join(self._dir(), 'description')
315 # where this series keeps its patches
316 self.__patch_dir = os.path.join(self._dir(), 'patches')
317 if not os.path.isdir(self.__patch_dir):
318 self.__patch_dir = self._dir()
320 # if no __refs_dir, create and populate it (upgrade old repositories)
321 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
322 os.makedirs(self.__refs_dir)
323 for patch in self.get_applied() + self.get_unapplied():
324 self.get_patch(patch).update_top_ref()
327 self.__trash_dir = os.path.join(self._dir(), 'trash')
328 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
329 os.makedirs(self.__trash_dir)
331 def __patch_name_valid(self, name):
332 """Raise an exception if the patch name is not valid.
334 if not name or re.search('[^\w.-]', name):
335 raise StackException, 'Invalid patch name: "%s"' % name
337 def get_branch(self):
338 """Return the branch name for the Series object
342 def __set_current(self, name):
343 """Sets the topmost patch
345 self._set_field('current', name)
347 def get_patch(self, name):
348 """Return a Patch object for the given name
350 return Patch(name, self.__patch_dir, self.__refs_dir)
352 def get_current_patch(self):
353 """Return a Patch object representing the topmost patch, or
354 None if there is no such patch."""
355 crt = self.get_current()
358 return Patch(crt, self.__patch_dir, self.__refs_dir)
360 def get_current(self):
361 """Return the name of the topmost patch, or None if there is
363 name = self._get_field('current')
369 def get_applied(self):
370 if not os.path.isfile(self.__applied_file):
371 raise StackException, 'Branch "%s" not initialised' % self.__name
372 f = file(self.__applied_file)
373 names = [line.strip() for line in f.readlines()]
377 def get_unapplied(self):
378 if not os.path.isfile(self.__unapplied_file):
379 raise StackException, 'Branch "%s" not initialised' % self.__name
380 f = file(self.__unapplied_file)
381 names = [line.strip() for line in f.readlines()]
385 def get_hidden(self):
386 if not os.path.isfile(self.__hidden_file):
388 f = file(self.__hidden_file)
389 names = [line.strip() for line in f.readlines()]
393 def get_base_file(self):
394 self.__begin_stack_check()
395 return self.__base_file
397 def get_protected(self):
398 return os.path.isfile(os.path.join(self._dir(), 'protected'))
401 protect_file = os.path.join(self._dir(), 'protected')
402 if not os.path.isfile(protect_file):
403 create_empty_file(protect_file)
406 protect_file = os.path.join(self._dir(), 'protected')
407 if os.path.isfile(protect_file):
408 os.remove(protect_file)
410 def get_description(self):
411 return self._get_field('description') or ''
413 def set_description(self, line):
414 self._set_field('description', line)
416 def get_parent_remote(self):
417 value = config.get('branch.%s.remote' % self.__name)
420 elif 'origin' in git.remotes_list():
421 print 'Notice: no parent remote declared for stack "%s", ' \
422 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
423 'and "branch.%s.merge" with "git repo-config".' \
424 % (self.__name, self.__name, self.__name)
427 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
429 def __set_parent_remote(self, remote):
430 value = config.set('branch.%s.remote' % self.__name, remote)
432 def get_parent_branch(self):
433 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
436 elif git.rev_parse('heads/origin'):
437 print 'Notice: no parent branch declared for stack "%s", ' \
438 'defaulting to "heads/origin". Consider setting ' \
439 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
440 % (self.__name, self.__name)
441 return 'heads/origin'
443 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
445 def __set_parent_branch(self, name):
446 if config.get('branch.%s.remote' % self.__name):
447 # Never set merge if remote is not set to avoid
448 # possibly-erroneous lookups into 'origin'
449 config.set('branch.%s.merge' % self.__name, name)
450 config.set('branch.%s.stgit.parentbranch' % self.__name, name)
452 def set_parent(self, remote, localbranch):
453 # policy: record local branches as remote='.'
454 recordremote = remote or '.'
456 self.__set_parent_remote(recordremote)
457 self.__set_parent_branch(localbranch)
458 # We'll enforce this later
460 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
462 def __patch_is_current(self, patch):
463 return patch.get_name() == self.get_current()
465 def patch_applied(self, name):
466 """Return true if the patch exists in the applied list
468 return name in self.get_applied()
470 def patch_unapplied(self, name):
471 """Return true if the patch exists in the unapplied list
473 return name in self.get_unapplied()
475 def patch_hidden(self, name):
476 """Return true if the patch is hidden.
478 return name in self.get_hidden()
480 def patch_exists(self, name):
481 """Return true if there is a patch with the given name, false
483 return self.patch_applied(name) or self.patch_unapplied(name)
485 def __begin_stack_check(self):
486 """Save the current HEAD into .git/refs/heads/base if the stack
489 if len(self.get_applied()) == 0:
490 head = git.get_head()
491 write_string(self.__base_file, head)
493 def __end_stack_check(self):
494 """Remove .git/refs/heads/base if the stack is empty.
495 This warning should never happen
497 if len(self.get_applied()) == 0 \
498 and read_string(self.__base_file) != git.get_head():
499 print 'Warning: stack empty but the HEAD and base are different'
501 def head_top_equal(self):
502 """Return true if the head and the top are the same
504 crt = self.get_current_patch()
506 # we don't care, no patches applied
508 return git.get_head() == crt.get_top()
510 def is_initialised(self):
511 """Checks if series is already initialised
513 return os.path.isdir(self.__patch_dir)
515 def init(self, create_at=False, parent_remote=None, parent_branch=None):
516 """Initialises the stgit series
518 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
520 if os.path.exists(self.__patch_dir):
521 raise StackException, self.__patch_dir + ' already exists'
522 if os.path.exists(self.__refs_dir):
523 raise StackException, self.__refs_dir + ' already exists'
524 if os.path.exists(self.__base_file):
525 raise StackException, self.__base_file + ' already exists'
527 if (create_at!=False):
528 git.create_branch(self.__name, create_at)
530 os.makedirs(self.__patch_dir)
532 self.set_parent(parent_remote, parent_branch)
534 create_dirs(bases_dir)
536 self.create_empty_field('applied')
537 self.create_empty_field('unapplied')
538 self.create_empty_field('description')
539 os.makedirs(os.path.join(self._dir(), 'patches'))
540 os.makedirs(self.__refs_dir)
541 self.__begin_stack_check()
544 """Either convert to use a separate patch directory, or
545 unconvert to place the patches in the same directory with
548 if self.__patch_dir == self._dir():
549 print 'Converting old-style to new-style...',
552 self.__patch_dir = os.path.join(self._dir(), 'patches')
553 os.makedirs(self.__patch_dir)
555 for p in self.get_applied() + self.get_unapplied():
556 src = os.path.join(self._dir(), p)
557 dest = os.path.join(self.__patch_dir, p)
563 print 'Converting new-style to old-style...',
566 for p in self.get_applied() + self.get_unapplied():
567 src = os.path.join(self.__patch_dir, p)
568 dest = os.path.join(self._dir(), p)
571 if not os.listdir(self.__patch_dir):
572 os.rmdir(self.__patch_dir)
575 print 'Patch directory %s is not empty.' % self.__name
577 self.__patch_dir = self._dir()
579 def rename(self, to_name):
582 to_stack = Series(to_name)
584 if to_stack.is_initialised():
585 raise StackException, '"%s" already exists' % to_stack.get_branch()
586 if os.path.exists(to_stack.__base_file):
587 os.remove(to_stack.__base_file)
589 git.rename_branch(self.__name, to_name)
591 if os.path.isdir(self._dir()):
592 rename(os.path.join(self.__base_dir, 'patches'),
593 self.__name, to_stack.__name)
594 if os.path.exists(self.__base_file):
595 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
596 self.__name, to_stack.__name)
597 if os.path.exists(self.__refs_dir):
598 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
599 self.__name, to_stack.__name)
601 # Rename the config section
602 config.rename_section("branch.%s" % self.__name,
603 "branch.%s" % to_name)
605 self.__init__(to_name)
607 def clone(self, target_series):
611 # allow cloning of branches not under StGIT control
612 base = read_string(self.get_base_file())
614 base = git.get_head()
615 Series(target_series).init(create_at = base)
616 new_series = Series(target_series)
618 # generate an artificial description file
619 new_series.set_description('clone of "%s"' % self.__name)
621 # clone self's entire series as unapplied patches
623 # allow cloning of branches not under StGIT control
624 applied = self.get_applied()
625 unapplied = self.get_unapplied()
626 patches = applied + unapplied
629 patches = applied = unapplied = []
631 patch = self.get_patch(p)
632 new_series.new_patch(p, message = patch.get_description(),
633 can_edit = False, unapplied = True,
634 bottom = patch.get_bottom(),
635 top = patch.get_top(),
636 author_name = patch.get_authname(),
637 author_email = patch.get_authemail(),
638 author_date = patch.get_authdate())
640 # fast forward the cloned series to self's top
641 new_series.forward_patches(applied)
643 # Clone remote and merge settings
644 value = config.get('branch.%s.remote' % self.__name)
646 config.set('branch.%s.remote' % target_series, value)
648 value = config.get('branch.%s.merge' % self.__name)
650 config.set('branch.%s.merge' % target_series, value)
652 def delete(self, force = False):
653 """Deletes an stgit series
655 if self.is_initialised():
656 patches = self.get_unapplied() + self.get_applied()
657 if not force and patches:
658 raise StackException, \
659 'Cannot delete: the series still contains patches'
661 Patch(p, self.__patch_dir, self.__refs_dir).delete()
663 # remove the trash directory
664 for fname in os.listdir(self.__trash_dir):
666 os.rmdir(self.__trash_dir)
668 # FIXME: find a way to get rid of those manual removals
669 # (move functionality to StgitObject ?)
670 if os.path.exists(self.__applied_file):
671 os.remove(self.__applied_file)
672 if os.path.exists(self.__unapplied_file):
673 os.remove(self.__unapplied_file)
674 if os.path.exists(self.__hidden_file):
675 os.remove(self.__hidden_file)
676 if os.path.exists(self.__current_file):
677 os.remove(self.__current_file)
678 if os.path.exists(self.__descr_file):
679 os.remove(self.__descr_file)
680 if not os.listdir(self.__patch_dir):
681 os.rmdir(self.__patch_dir)
683 print 'Patch directory %s is not empty.' % self.__name
684 if not os.listdir(self._dir()):
685 remove_dirs(os.path.join(self.__base_dir, 'patches'),
688 print 'Series directory %s is not empty.' % self.__name
689 if not os.listdir(self.__refs_dir):
690 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
693 print 'Refs directory %s is not empty.' % self.__refs_dir
695 if os.path.exists(self.__base_file):
696 remove_file_and_dirs(
697 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
699 def refresh_patch(self, files = None, message = None, edit = False,
702 author_name = None, author_email = None,
704 committer_name = None, committer_email = None,
705 backup = False, sign_str = None, log = 'refresh'):
706 """Generates a new commit for the given patch
708 name = self.get_current()
710 raise StackException, 'No patches applied'
712 patch = Patch(name, self.__patch_dir, self.__refs_dir)
714 descr = patch.get_description()
715 if not (message or descr):
721 if not message and edit:
722 descr = edit_file(self, descr.rstrip(), \
723 'Please edit the description for patch "%s" ' \
724 'above.' % name, show_patch)
727 author_name = patch.get_authname()
729 author_email = patch.get_authemail()
731 author_date = patch.get_authdate()
732 if not committer_name:
733 committer_name = patch.get_commname()
734 if not committer_email:
735 committer_email = patch.get_commemail()
738 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
739 committer_name, committer_email)
741 bottom = patch.get_bottom()
743 commit_id = git.commit(files = files,
744 message = descr, parents = [bottom],
745 cache_update = cache_update,
747 author_name = author_name,
748 author_email = author_email,
749 author_date = author_date,
750 committer_name = committer_name,
751 committer_email = committer_email)
753 patch.set_bottom(bottom, backup = backup)
754 patch.set_top(commit_id, backup = backup)
755 patch.set_description(descr)
756 patch.set_authname(author_name)
757 patch.set_authemail(author_email)
758 patch.set_authdate(author_date)
759 patch.set_commname(committer_name)
760 patch.set_commemail(committer_email)
763 self.log_patch(patch, log)
767 def undo_refresh(self):
768 """Undo the patch boundaries changes caused by 'refresh'
770 name = self.get_current()
773 patch = Patch(name, self.__patch_dir, self.__refs_dir)
774 old_bottom = patch.get_old_bottom()
775 old_top = patch.get_old_top()
777 # the bottom of the patch is not changed by refresh. If the
778 # old_bottom is different, there wasn't any previous 'refresh'
779 # command (probably only a 'push')
780 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
781 raise StackException, 'No undo information available'
783 git.reset(tree_id = old_top, check_out = False)
784 if patch.restore_old_boundaries():
785 self.log_patch(patch, 'undo')
787 def new_patch(self, name, message = None, can_edit = True,
788 unapplied = False, show_patch = False,
789 top = None, bottom = None,
790 author_name = None, author_email = None, author_date = None,
791 committer_name = None, committer_email = None,
792 before_existing = False, refresh = True):
793 """Creates a new patch
795 self.__patch_name_valid(name)
797 if self.patch_applied(name) or self.patch_unapplied(name):
798 raise StackException, 'Patch "%s" already exists' % name
800 if not message and can_edit:
801 descr = edit_file(self, None, \
802 'Please enter the description for patch "%s" ' \
803 'above.' % name, show_patch)
807 head = git.get_head()
809 self.__begin_stack_check()
811 patch = Patch(name, self.__patch_dir, self.__refs_dir)
815 patch.set_bottom(bottom)
817 patch.set_bottom(head)
823 patch.set_description(descr)
824 patch.set_authname(author_name)
825 patch.set_authemail(author_email)
826 patch.set_authdate(author_date)
827 patch.set_commname(committer_name)
828 patch.set_commemail(committer_email)
831 self.log_patch(patch, 'new')
833 patches = [patch.get_name()] + self.get_unapplied()
835 f = file(self.__unapplied_file, 'w+')
836 f.writelines([line + '\n' for line in patches])
838 elif before_existing:
839 self.log_patch(patch, 'new')
841 insert_string(self.__applied_file, patch.get_name())
842 if not self.get_current():
843 self.__set_current(name)
845 append_string(self.__applied_file, patch.get_name())
846 self.__set_current(name)
848 self.refresh_patch(cache_update = False, log = 'new')
850 def delete_patch(self, name):
853 self.__patch_name_valid(name)
854 patch = Patch(name, self.__patch_dir, self.__refs_dir)
856 if self.__patch_is_current(patch):
858 elif self.patch_applied(name):
859 raise StackException, 'Cannot remove an applied patch, "%s", ' \
860 'which is not current' % name
861 elif not name in self.get_unapplied():
862 raise StackException, 'Unknown patch "%s"' % name
864 # save the commit id to a trash file
865 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
869 unapplied = self.get_unapplied()
870 unapplied.remove(name)
871 f = file(self.__unapplied_file, 'w+')
872 f.writelines([line + '\n' for line in unapplied])
875 if self.patch_hidden(name):
876 self.unhide_patch(name)
878 self.__begin_stack_check()
880 def forward_patches(self, names):
881 """Try to fast-forward an array of patches.
883 On return, patches in names[0:returned_value] have been pushed on the
884 stack. Apply the rest with push_patch
886 unapplied = self.get_unapplied()
887 self.__begin_stack_check()
893 assert(name in unapplied)
895 patch = Patch(name, self.__patch_dir, self.__refs_dir)
898 bottom = patch.get_bottom()
899 top = patch.get_top()
901 # top != bottom always since we have a commit for each patch
903 # reset the backup information. No logging since the
904 # patch hasn't changed
905 patch.set_bottom(head, backup = True)
906 patch.set_top(top, backup = True)
909 head_tree = git.get_commit(head).get_tree()
910 bottom_tree = git.get_commit(bottom).get_tree()
911 if head_tree == bottom_tree:
912 # We must just reparent this patch and create a new commit
914 descr = patch.get_description()
915 author_name = patch.get_authname()
916 author_email = patch.get_authemail()
917 author_date = patch.get_authdate()
918 committer_name = patch.get_commname()
919 committer_email = patch.get_commemail()
921 top_tree = git.get_commit(top).get_tree()
923 top = git.commit(message = descr, parents = [head],
924 cache_update = False,
927 author_name = author_name,
928 author_email = author_email,
929 author_date = author_date,
930 committer_name = committer_name,
931 committer_email = committer_email)
933 patch.set_bottom(head, backup = True)
934 patch.set_top(top, backup = True)
936 self.log_patch(patch, 'push(f)')
939 # stop the fast-forwarding, must do a real merge
943 unapplied.remove(name)
950 append_strings(self.__applied_file, names[0:forwarded])
952 f = file(self.__unapplied_file, 'w+')
953 f.writelines([line + '\n' for line in unapplied])
956 self.__set_current(name)
960 def merged_patches(self, names):
961 """Test which patches were merged upstream by reverse-applying
962 them in reverse order. The function returns the list of
963 patches detected to have been applied. The state of the tree
964 is restored to the original one
966 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
972 if git.apply_diff(p.get_top(), p.get_bottom()):
973 merged.append(p.get_name())
980 def push_patch(self, name, empty = False):
981 """Pushes a patch on the stack
983 unapplied = self.get_unapplied()
984 assert(name in unapplied)
986 self.__begin_stack_check()
988 patch = Patch(name, self.__patch_dir, self.__refs_dir)
990 head = git.get_head()
991 bottom = patch.get_bottom()
992 top = patch.get_top()
997 # top != bottom always since we have a commit for each patch
999 # just make an empty patch (top = bottom = HEAD). This
1000 # option is useful to allow undoing already merged
1001 # patches. The top is updated by refresh_patch since we
1002 # need an empty commit
1003 patch.set_bottom(head, backup = True)
1004 patch.set_top(head, backup = True)
1006 elif head == bottom:
1007 # reset the backup information. No need for logging
1008 patch.set_bottom(bottom, backup = True)
1009 patch.set_top(top, backup = True)
1013 # new patch needs to be refreshed.
1014 # The current patch is empty after merge.
1015 patch.set_bottom(head, backup = True)
1016 patch.set_top(head, backup = True)
1018 # Try the fast applying first. If this fails, fall back to the
1020 if not git.apply_diff(bottom, top):
1021 # if git.apply_diff() fails, the patch requires a diff3
1022 # merge and can be reported as modified
1025 # merge can fail but the patch needs to be pushed
1027 git.merge(bottom, head, top, recursive = True)
1028 except git.GitException, ex:
1029 print >> sys.stderr, \
1030 'The merge failed during "push". ' \
1031 'Use "refresh" after fixing the conflicts'
1033 append_string(self.__applied_file, name)
1035 unapplied.remove(name)
1036 f = file(self.__unapplied_file, 'w+')
1037 f.writelines([line + '\n' for line in unapplied])
1040 self.__set_current(name)
1042 # head == bottom case doesn't need to refresh the patch
1043 if empty or head != bottom:
1045 # if the merge was OK and no conflicts, just refresh the patch
1046 # The GIT cache was already updated by the merge operation
1051 self.refresh_patch(cache_update = False, log = log)
1053 # we store the correctly merged files only for
1054 # tracking the conflict history. Note that the
1055 # git.merge() operations should always leave the index
1056 # in a valid state (i.e. only stage 0 files)
1057 self.refresh_patch(cache_update = False, log = 'push(c)')
1058 raise StackException, str(ex)
1062 def undo_push(self):
1063 name = self.get_current()
1066 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1067 old_bottom = patch.get_old_bottom()
1068 old_top = patch.get_old_top()
1070 # the top of the patch is changed by a push operation only
1071 # together with the bottom (otherwise the top was probably
1072 # modified by 'refresh'). If they are both unchanged, there
1073 # was a fast forward
1074 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1075 raise StackException, 'No undo information available'
1078 self.pop_patch(name)
1079 ret = patch.restore_old_boundaries()
1081 self.log_patch(patch, 'undo')
1085 def pop_patch(self, name, keep = False):
1086 """Pops the top patch from the stack
1088 applied = self.get_applied()
1090 assert(name in applied)
1092 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1094 # only keep the local changes
1095 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1096 raise StackException, \
1097 'Failed to pop patches while preserving the local changes'
1099 git.switch(patch.get_bottom(), keep)
1101 # save the new applied list
1102 idx = applied.index(name) + 1
1104 popped = applied[:idx]
1106 unapplied = popped + self.get_unapplied()
1108 f = file(self.__unapplied_file, 'w+')
1109 f.writelines([line + '\n' for line in unapplied])
1115 f = file(self.__applied_file, 'w+')
1116 f.writelines([line + '\n' for line in applied])
1120 self.__set_current(None)
1122 self.__set_current(applied[-1])
1124 self.__end_stack_check()
1126 def empty_patch(self, name):
1127 """Returns True if the patch is empty
1129 self.__patch_name_valid(name)
1130 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1131 bottom = patch.get_bottom()
1132 top = patch.get_top()
1136 elif git.get_commit(top).get_tree() \
1137 == git.get_commit(bottom).get_tree():
1142 def rename_patch(self, oldname, newname):
1143 self.__patch_name_valid(newname)
1145 applied = self.get_applied()
1146 unapplied = self.get_unapplied()
1148 if oldname == newname:
1149 raise StackException, '"To" name and "from" name are the same'
1151 if newname in applied or newname in unapplied:
1152 raise StackException, 'Patch "%s" already exists' % newname
1154 if self.patch_hidden(oldname):
1155 self.unhide_patch(oldname)
1156 self.hide_patch(newname)
1158 if oldname in unapplied:
1159 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1160 unapplied[unapplied.index(oldname)] = newname
1162 f = file(self.__unapplied_file, 'w+')
1163 f.writelines([line + '\n' for line in unapplied])
1165 elif oldname in applied:
1166 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1167 if oldname == self.get_current():
1168 self.__set_current(newname)
1170 applied[applied.index(oldname)] = newname
1172 f = file(self.__applied_file, 'w+')
1173 f.writelines([line + '\n' for line in applied])
1176 raise StackException, 'Unknown patch "%s"' % oldname
1178 def log_patch(self, patch, message):
1179 """Generate a log commit for a patch
1181 top = git.get_commit(patch.get_top())
1182 msg = '%s\t%s' % (message, top.get_id_hash())
1184 old_log = patch.get_log()
1190 log = git.commit(message = msg, parents = parents,
1191 cache_update = False, tree_id = top.get_tree(),
1195 def hide_patch(self, name):
1196 """Add the patch to the hidden list.
1198 if not self.patch_exists(name):
1199 raise StackException, 'Unknown patch "%s"' % name
1200 elif self.patch_hidden(name):
1201 raise StackException, 'Patch "%s" already hidden' % name
1203 append_string(self.__hidden_file, name)
1205 def unhide_patch(self, name):
1206 """Add the patch to the hidden list.
1208 if not self.patch_exists(name):
1209 raise StackException, 'Unknown patch "%s"' % name
1210 hidden = self.get_hidden()
1211 if not name in hidden:
1212 raise StackException, 'Patch "%s" not hidden' % name
1216 f = file(self.__hidden_file, 'w+')
1217 f.writelines([line + '\n' for line in hidden])