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:'
112 """An object with stgit-like properties stored as files in a directory
114 def _set_dir(self, dir):
119 def create_empty_field(self, name):
120 create_empty_file(os.path.join(self.__dir, name))
122 def _get_field(self, name, multiline = False):
123 id_file = os.path.join(self.__dir, name)
124 if os.path.isfile(id_file):
125 line = read_string(id_file, multiline)
133 def _set_field(self, name, value, multiline = False):
134 fname = os.path.join(self.__dir, name)
135 if value and value != '':
136 write_string(fname, value, multiline)
137 elif os.path.isfile(fname):
141 class Patch(StgitObject):
142 """Basic patch implementation
144 def __init__(self, name, series_dir, refs_dir):
145 self.__series_dir = series_dir
147 self._set_dir(os.path.join(self.__series_dir, self.__name))
148 self.__refs_dir = refs_dir
149 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
150 self.__log_ref_file = os.path.join(self.__refs_dir,
151 self.__name + '.log')
154 os.mkdir(self._dir())
155 self.create_empty_field('bottom')
156 self.create_empty_field('top')
159 for f in os.listdir(self._dir()):
160 os.remove(os.path.join(self._dir(), f))
161 os.rmdir(self._dir())
162 os.remove(self.__top_ref_file)
163 if os.path.exists(self.__log_ref_file):
164 os.remove(self.__log_ref_file)
169 def rename(self, newname):
171 old_top_ref_file = self.__top_ref_file
172 old_log_ref_file = self.__log_ref_file
173 self.__name = newname
174 self._set_dir(os.path.join(self.__series_dir, self.__name))
175 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
176 self.__log_ref_file = os.path.join(self.__refs_dir,
177 self.__name + '.log')
179 os.rename(olddir, self._dir())
180 os.rename(old_top_ref_file, self.__top_ref_file)
181 if os.path.exists(old_log_ref_file):
182 os.rename(old_log_ref_file, self.__log_ref_file)
184 def __update_top_ref(self, ref):
185 write_string(self.__top_ref_file, ref)
187 def __update_log_ref(self, ref):
188 write_string(self.__log_ref_file, ref)
190 def update_top_ref(self):
193 self.__update_top_ref(top)
195 def get_old_bottom(self):
196 return self._get_field('bottom.old')
198 def get_bottom(self):
199 return self._get_field('bottom')
201 def set_bottom(self, value, backup = False):
203 curr = self._get_field('bottom')
204 self._set_field('bottom.old', curr)
205 self._set_field('bottom', value)
207 def get_old_top(self):
208 return self._get_field('top.old')
211 return self._get_field('top')
213 def set_top(self, value, backup = False):
215 curr = self._get_field('top')
216 self._set_field('top.old', curr)
217 self._set_field('top', value)
218 self.__update_top_ref(value)
220 def restore_old_boundaries(self):
221 bottom = self._get_field('bottom.old')
222 top = self._get_field('top.old')
225 self._set_field('bottom', bottom)
226 self._set_field('top', top)
227 self.__update_top_ref(top)
232 def get_description(self):
233 return self._get_field('description', True)
235 def set_description(self, line):
236 self._set_field('description', line, True)
238 def get_authname(self):
239 return self._get_field('authname')
241 def set_authname(self, name):
242 self._set_field('authname', name or git.author().name)
244 def get_authemail(self):
245 return self._get_field('authemail')
247 def set_authemail(self, email):
248 self._set_field('authemail', email or git.author().email)
250 def get_authdate(self):
251 return self._get_field('authdate')
253 def set_authdate(self, date):
254 self._set_field('authdate', date or git.author().date)
256 def get_commname(self):
257 return self._get_field('commname')
259 def set_commname(self, name):
260 self._set_field('commname', name or git.committer().name)
262 def get_commemail(self):
263 return self._get_field('commemail')
265 def set_commemail(self, email):
266 self._set_field('commemail', email or git.committer().email)
269 return self._get_field('log')
271 def set_log(self, value, backup = False):
272 self._set_field('log', value)
273 self.__update_log_ref(value)
276 class Series(StgitObject):
277 """Class including the operations on series
279 def __init__(self, name = None):
280 """Takes a series name as the parameter.
286 self.__name = git.get_head_file()
287 self.__base_dir = basedir.get()
288 except git.GitException, ex:
289 raise StackException, 'GIT tree not initialised: %s' % ex
291 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
292 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
294 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
297 self.__applied_file = os.path.join(self._dir(), 'applied')
298 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
299 self.__hidden_file = os.path.join(self._dir(), 'hidden')
300 self.__current_file = os.path.join(self._dir(), 'current')
301 self.__descr_file = os.path.join(self._dir(), 'description')
303 # where this series keeps its patches
304 self.__patch_dir = os.path.join(self._dir(), 'patches')
305 if not os.path.isdir(self.__patch_dir):
306 self.__patch_dir = self._dir()
308 # if no __refs_dir, create and populate it (upgrade old repositories)
309 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
310 os.makedirs(self.__refs_dir)
311 for patch in self.get_applied() + self.get_unapplied():
312 self.get_patch(patch).update_top_ref()
315 self.__trash_dir = os.path.join(self._dir(), 'trash')
316 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
317 os.makedirs(self.__trash_dir)
319 def __patch_name_valid(self, name):
320 """Raise an exception if the patch name is not valid.
322 if not name or re.search('[^\w.-]', name):
323 raise StackException, 'Invalid patch name: "%s"' % name
325 def get_branch(self):
326 """Return the branch name for the Series object
330 def __set_current(self, name):
331 """Sets the topmost patch
333 self._set_field('current', name)
335 def get_patch(self, name):
336 """Return a Patch object for the given name
338 return Patch(name, self.__patch_dir, self.__refs_dir)
340 def get_current_patch(self):
341 """Return a Patch object representing the topmost patch, or
342 None if there is no such patch."""
343 crt = self.get_current()
346 return Patch(crt, self.__patch_dir, self.__refs_dir)
348 def get_current(self):
349 """Return the name of the topmost patch, or None if there is
351 name = self._get_field('current')
357 def get_applied(self):
358 if not os.path.isfile(self.__applied_file):
359 raise StackException, 'Branch "%s" not initialised' % self.__name
360 f = file(self.__applied_file)
361 names = [line.strip() for line in f.readlines()]
365 def get_unapplied(self):
366 if not os.path.isfile(self.__unapplied_file):
367 raise StackException, 'Branch "%s" not initialised' % self.__name
368 f = file(self.__unapplied_file)
369 names = [line.strip() for line in f.readlines()]
373 def get_hidden(self):
374 if not os.path.isfile(self.__hidden_file):
376 f = file(self.__hidden_file)
377 names = [line.strip() for line in f.readlines()]
381 def get_base_file(self):
382 self.__begin_stack_check()
383 return self.__base_file
386 return read_string(self.get_base_file())
389 """Return the head of the branch
391 crt = self.get_current_patch()
395 return self.get_base()
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 if os.path.exists(self.__patch_dir):
519 raise StackException, self.__patch_dir + ' already exists'
520 if os.path.exists(self.__refs_dir):
521 raise StackException, self.__refs_dir + ' already exists'
522 if os.path.exists(self.__base_file):
523 raise StackException, self.__base_file + ' already exists'
525 if (create_at!=False):
526 git.create_branch(self.__name, create_at)
528 os.makedirs(self.__patch_dir)
530 self.set_parent(parent_remote, parent_branch)
532 create_dirs(os.path.join(self.__base_dir, 'refs', 'bases'))
534 self.create_empty_field('applied')
535 self.create_empty_field('unapplied')
536 self.create_empty_field('description')
537 os.makedirs(os.path.join(self._dir(), 'patches'))
538 os.makedirs(self.__refs_dir)
539 self.__begin_stack_check()
540 self._set_field('orig-base', git.get_head())
543 """Either convert to use a separate patch directory, or
544 unconvert to place the patches in the same directory with
547 if self.__patch_dir == self._dir():
548 print 'Converting old-style to new-style...',
551 self.__patch_dir = os.path.join(self._dir(), 'patches')
552 os.makedirs(self.__patch_dir)
554 for p in self.get_applied() + self.get_unapplied():
555 src = os.path.join(self._dir(), p)
556 dest = os.path.join(self.__patch_dir, p)
562 print 'Converting new-style to old-style...',
565 for p in self.get_applied() + self.get_unapplied():
566 src = os.path.join(self.__patch_dir, p)
567 dest = os.path.join(self._dir(), p)
570 if not os.listdir(self.__patch_dir):
571 os.rmdir(self.__patch_dir)
574 print 'Patch directory %s is not empty.' % self.__patch_dir
576 self.__patch_dir = self._dir()
578 def rename(self, to_name):
581 to_stack = Series(to_name)
583 if to_stack.is_initialised():
584 raise StackException, '"%s" already exists' % to_stack.get_branch()
585 if os.path.exists(to_stack.__base_file):
586 os.remove(to_stack.__base_file)
588 git.rename_branch(self.__name, to_name)
590 if os.path.isdir(self._dir()):
591 rename(os.path.join(self.__base_dir, 'patches'),
592 self.__name, to_stack.__name)
593 if os.path.exists(self.__base_file):
594 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
595 self.__name, to_stack.__name)
596 if os.path.exists(self.__refs_dir):
597 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
598 self.__name, to_stack.__name)
600 # Rename the config section
601 config.rename_section("branch.%s" % self.__name,
602 "branch.%s" % to_name)
604 self.__init__(to_name)
606 def clone(self, target_series):
610 # allow cloning of branches not under StGIT control
611 base = self.get_base()
613 base = git.get_head()
614 Series(target_series).init(create_at = base)
615 new_series = Series(target_series)
617 # generate an artificial description file
618 new_series.set_description('clone of "%s"' % self.__name)
620 # clone self's entire series as unapplied patches
622 # allow cloning of branches not under StGIT control
623 applied = self.get_applied()
624 unapplied = self.get_unapplied()
625 patches = applied + unapplied
628 patches = applied = unapplied = []
630 patch = self.get_patch(p)
631 new_series.new_patch(p, message = patch.get_description(),
632 can_edit = False, unapplied = True,
633 bottom = patch.get_bottom(),
634 top = patch.get_top(),
635 author_name = patch.get_authname(),
636 author_email = patch.get_authemail(),
637 author_date = patch.get_authdate())
639 # fast forward the cloned series to self's top
640 new_series.forward_patches(applied)
642 # Clone parent informations
643 value = config.get('branch.%s.remote' % self.__name)
645 config.set('branch.%s.remote' % target_series, value)
647 value = config.get('branch.%s.merge' % self.__name)
649 config.set('branch.%s.merge' % target_series, value)
651 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
653 config.set('branch.%s.stgit.parentbranch' % target_series, value)
655 def delete(self, force = False):
656 """Deletes an stgit series
658 if self.is_initialised():
659 patches = self.get_unapplied() + self.get_applied()
660 if not force and patches:
661 raise StackException, \
662 'Cannot delete: the series still contains patches'
664 Patch(p, self.__patch_dir, self.__refs_dir).delete()
666 # remove the trash directory
667 for fname in os.listdir(self.__trash_dir):
669 os.rmdir(self.__trash_dir)
671 # FIXME: find a way to get rid of those manual removals
672 # (move functionality to StgitObject ?)
673 if os.path.exists(self.__applied_file):
674 os.remove(self.__applied_file)
675 if os.path.exists(self.__unapplied_file):
676 os.remove(self.__unapplied_file)
677 if os.path.exists(self.__hidden_file):
678 os.remove(self.__hidden_file)
679 if os.path.exists(self.__current_file):
680 os.remove(self.__current_file)
681 if os.path.exists(self.__descr_file):
682 os.remove(self.__descr_file)
683 if os.path.exists(self._dir()+'/orig-base'):
684 os.remove(self._dir()+'/orig-base')
686 if not os.listdir(self.__patch_dir):
687 os.rmdir(self.__patch_dir)
689 print 'Patch directory %s is not empty.' % self.__patch_dir
692 os.removedirs(self._dir())
694 raise StackException, 'Series directory %s is not empty.' % self._dir()
697 os.removedirs(self.__refs_dir)
699 print 'Refs directory %s is not empty.' % self.__refs_dir
701 if os.path.exists(self.__base_file):
702 remove_file_and_dirs(
703 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
705 # Cleanup parent informations
706 # FIXME: should one day make use of git-config --section-remove,
707 # scheduled for 1.5.1
708 config.unset('branch.%s.remote' % self.__name)
709 config.unset('branch.%s.merge' % self.__name)
710 config.unset('branch.%s.stgit.parentbranch' % self.__name)
712 def refresh_patch(self, files = None, message = None, edit = False,
715 author_name = None, author_email = None,
717 committer_name = None, committer_email = None,
718 backup = False, sign_str = None, log = 'refresh'):
719 """Generates a new commit for the given patch
721 name = self.get_current()
723 raise StackException, 'No patches applied'
725 patch = Patch(name, self.__patch_dir, self.__refs_dir)
727 descr = patch.get_description()
728 if not (message or descr):
734 if not message and edit:
735 descr = edit_file(self, descr.rstrip(), \
736 'Please edit the description for patch "%s" ' \
737 'above.' % name, show_patch)
740 author_name = patch.get_authname()
742 author_email = patch.get_authemail()
744 author_date = patch.get_authdate()
745 if not committer_name:
746 committer_name = patch.get_commname()
747 if not committer_email:
748 committer_email = patch.get_commemail()
751 descr = descr.rstrip()
752 if descr.find("\nSigned-off-by:") < 0 \
753 and descr.find("\nAcked-by:") < 0:
756 descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
757 committer_name, committer_email)
759 bottom = patch.get_bottom()
761 commit_id = git.commit(files = files,
762 message = descr, parents = [bottom],
763 cache_update = cache_update,
765 author_name = author_name,
766 author_email = author_email,
767 author_date = author_date,
768 committer_name = committer_name,
769 committer_email = committer_email)
771 patch.set_bottom(bottom, backup = backup)
772 patch.set_top(commit_id, backup = backup)
773 patch.set_description(descr)
774 patch.set_authname(author_name)
775 patch.set_authemail(author_email)
776 patch.set_authdate(author_date)
777 patch.set_commname(committer_name)
778 patch.set_commemail(committer_email)
781 self.log_patch(patch, log)
785 def undo_refresh(self):
786 """Undo the patch boundaries changes caused by 'refresh'
788 name = self.get_current()
791 patch = Patch(name, self.__patch_dir, self.__refs_dir)
792 old_bottom = patch.get_old_bottom()
793 old_top = patch.get_old_top()
795 # the bottom of the patch is not changed by refresh. If the
796 # old_bottom is different, there wasn't any previous 'refresh'
797 # command (probably only a 'push')
798 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
799 raise StackException, 'No undo information available'
801 git.reset(tree_id = old_top, check_out = False)
802 if patch.restore_old_boundaries():
803 self.log_patch(patch, 'undo')
805 def new_patch(self, name, message = None, can_edit = True,
806 unapplied = False, show_patch = False,
807 top = None, bottom = None,
808 author_name = None, author_email = None, author_date = None,
809 committer_name = None, committer_email = None,
810 before_existing = False, refresh = True):
811 """Creates a new patch
813 self.__patch_name_valid(name)
815 if self.patch_applied(name) or self.patch_unapplied(name):
816 raise StackException, 'Patch "%s" already exists' % name
818 if not message and can_edit:
819 descr = edit_file(self, None, \
820 'Please enter the description for patch "%s" ' \
821 'above.' % name, show_patch)
825 head = git.get_head()
827 self.__begin_stack_check()
829 patch = Patch(name, self.__patch_dir, self.__refs_dir)
833 patch.set_bottom(bottom)
835 patch.set_bottom(head)
841 patch.set_description(descr)
842 patch.set_authname(author_name)
843 patch.set_authemail(author_email)
844 patch.set_authdate(author_date)
845 patch.set_commname(committer_name)
846 patch.set_commemail(committer_email)
849 self.log_patch(patch, 'new')
851 patches = [patch.get_name()] + self.get_unapplied()
853 f = file(self.__unapplied_file, 'w+')
854 f.writelines([line + '\n' for line in patches])
856 elif before_existing:
857 self.log_patch(patch, 'new')
859 insert_string(self.__applied_file, patch.get_name())
860 if not self.get_current():
861 self.__set_current(name)
863 append_string(self.__applied_file, patch.get_name())
864 self.__set_current(name)
866 self.refresh_patch(cache_update = False, log = 'new')
868 def delete_patch(self, name):
871 self.__patch_name_valid(name)
872 patch = Patch(name, self.__patch_dir, self.__refs_dir)
874 if self.__patch_is_current(patch):
876 elif self.patch_applied(name):
877 raise StackException, 'Cannot remove an applied patch, "%s", ' \
878 'which is not current' % name
879 elif not name in self.get_unapplied():
880 raise StackException, 'Unknown patch "%s"' % name
882 # save the commit id to a trash file
883 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
887 unapplied = self.get_unapplied()
888 unapplied.remove(name)
889 f = file(self.__unapplied_file, 'w+')
890 f.writelines([line + '\n' for line in unapplied])
893 if self.patch_hidden(name):
894 self.unhide_patch(name)
896 self.__begin_stack_check()
898 def forward_patches(self, names):
899 """Try to fast-forward an array of patches.
901 On return, patches in names[0:returned_value] have been pushed on the
902 stack. Apply the rest with push_patch
904 unapplied = self.get_unapplied()
905 self.__begin_stack_check()
911 assert(name in unapplied)
913 patch = Patch(name, self.__patch_dir, self.__refs_dir)
916 bottom = patch.get_bottom()
917 top = patch.get_top()
919 # top != bottom always since we have a commit for each patch
921 # reset the backup information. No logging since the
922 # patch hasn't changed
923 patch.set_bottom(head, backup = True)
924 patch.set_top(top, backup = True)
927 head_tree = git.get_commit(head).get_tree()
928 bottom_tree = git.get_commit(bottom).get_tree()
929 if head_tree == bottom_tree:
930 # We must just reparent this patch and create a new commit
932 descr = patch.get_description()
933 author_name = patch.get_authname()
934 author_email = patch.get_authemail()
935 author_date = patch.get_authdate()
936 committer_name = patch.get_commname()
937 committer_email = patch.get_commemail()
939 top_tree = git.get_commit(top).get_tree()
941 top = git.commit(message = descr, parents = [head],
942 cache_update = False,
945 author_name = author_name,
946 author_email = author_email,
947 author_date = author_date,
948 committer_name = committer_name,
949 committer_email = committer_email)
951 patch.set_bottom(head, backup = True)
952 patch.set_top(top, backup = True)
954 self.log_patch(patch, 'push(f)')
957 # stop the fast-forwarding, must do a real merge
961 unapplied.remove(name)
968 append_strings(self.__applied_file, names[0:forwarded])
970 f = file(self.__unapplied_file, 'w+')
971 f.writelines([line + '\n' for line in unapplied])
974 self.__set_current(name)
978 def merged_patches(self, names):
979 """Test which patches were merged upstream by reverse-applying
980 them in reverse order. The function returns the list of
981 patches detected to have been applied. The state of the tree
982 is restored to the original one
984 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
990 if git.apply_diff(p.get_top(), p.get_bottom()):
991 merged.append(p.get_name())
998 def push_patch(self, name, empty = False):
999 """Pushes a patch on the stack
1001 unapplied = self.get_unapplied()
1002 assert(name in unapplied)
1004 self.__begin_stack_check()
1006 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1008 head = git.get_head()
1009 bottom = patch.get_bottom()
1010 top = patch.get_top()
1015 # top != bottom always since we have a commit for each patch
1017 # just make an empty patch (top = bottom = HEAD). This
1018 # option is useful to allow undoing already merged
1019 # patches. The top is updated by refresh_patch since we
1020 # need an empty commit
1021 patch.set_bottom(head, backup = True)
1022 patch.set_top(head, backup = True)
1024 elif head == bottom:
1025 # reset the backup information. No need for logging
1026 patch.set_bottom(bottom, backup = True)
1027 patch.set_top(top, backup = True)
1031 # new patch needs to be refreshed.
1032 # The current patch is empty after merge.
1033 patch.set_bottom(head, backup = True)
1034 patch.set_top(head, backup = True)
1036 # Try the fast applying first. If this fails, fall back to the
1038 if not git.apply_diff(bottom, top):
1039 # if git.apply_diff() fails, the patch requires a diff3
1040 # merge and can be reported as modified
1043 # merge can fail but the patch needs to be pushed
1045 git.merge(bottom, head, top, recursive = True)
1046 except git.GitException, ex:
1047 print >> sys.stderr, \
1048 'The merge failed during "push". ' \
1049 'Use "refresh" after fixing the conflicts'
1051 append_string(self.__applied_file, name)
1053 unapplied.remove(name)
1054 f = file(self.__unapplied_file, 'w+')
1055 f.writelines([line + '\n' for line in unapplied])
1058 self.__set_current(name)
1060 # head == bottom case doesn't need to refresh the patch
1061 if empty or head != bottom:
1063 # if the merge was OK and no conflicts, just refresh the patch
1064 # The GIT cache was already updated by the merge operation
1069 self.refresh_patch(cache_update = False, log = log)
1071 # we store the correctly merged files only for
1072 # tracking the conflict history. Note that the
1073 # git.merge() operations should always leave the index
1074 # in a valid state (i.e. only stage 0 files)
1075 self.refresh_patch(cache_update = False, log = 'push(c)')
1076 raise StackException, str(ex)
1080 def undo_push(self):
1081 name = self.get_current()
1084 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1085 old_bottom = patch.get_old_bottom()
1086 old_top = patch.get_old_top()
1088 # the top of the patch is changed by a push operation only
1089 # together with the bottom (otherwise the top was probably
1090 # modified by 'refresh'). If they are both unchanged, there
1091 # was a fast forward
1092 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1093 raise StackException, 'No undo information available'
1096 self.pop_patch(name)
1097 ret = patch.restore_old_boundaries()
1099 self.log_patch(patch, 'undo')
1103 def pop_patch(self, name, keep = False):
1104 """Pops the top patch from the stack
1106 applied = self.get_applied()
1108 assert(name in applied)
1110 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1112 # only keep the local changes
1113 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1114 raise StackException, \
1115 'Failed to pop patches while preserving the local changes'
1117 git.switch(patch.get_bottom(), keep)
1119 # save the new applied list
1120 idx = applied.index(name) + 1
1122 popped = applied[:idx]
1124 unapplied = popped + self.get_unapplied()
1126 f = file(self.__unapplied_file, 'w+')
1127 f.writelines([line + '\n' for line in unapplied])
1133 f = file(self.__applied_file, 'w+')
1134 f.writelines([line + '\n' for line in applied])
1138 self.__set_current(None)
1140 self.__set_current(applied[-1])
1142 self.__end_stack_check()
1144 def empty_patch(self, name):
1145 """Returns True if the patch is empty
1147 self.__patch_name_valid(name)
1148 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1149 bottom = patch.get_bottom()
1150 top = patch.get_top()
1154 elif git.get_commit(top).get_tree() \
1155 == git.get_commit(bottom).get_tree():
1160 def rename_patch(self, oldname, newname):
1161 self.__patch_name_valid(newname)
1163 applied = self.get_applied()
1164 unapplied = self.get_unapplied()
1166 if oldname == newname:
1167 raise StackException, '"To" name and "from" name are the same'
1169 if newname in applied or newname in unapplied:
1170 raise StackException, 'Patch "%s" already exists' % newname
1172 if self.patch_hidden(oldname):
1173 self.unhide_patch(oldname)
1174 self.hide_patch(newname)
1176 if oldname in unapplied:
1177 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1178 unapplied[unapplied.index(oldname)] = newname
1180 f = file(self.__unapplied_file, 'w+')
1181 f.writelines([line + '\n' for line in unapplied])
1183 elif oldname in applied:
1184 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1185 if oldname == self.get_current():
1186 self.__set_current(newname)
1188 applied[applied.index(oldname)] = newname
1190 f = file(self.__applied_file, 'w+')
1191 f.writelines([line + '\n' for line in applied])
1194 raise StackException, 'Unknown patch "%s"' % oldname
1196 def log_patch(self, patch, message):
1197 """Generate a log commit for a patch
1199 top = git.get_commit(patch.get_top())
1200 msg = '%s\t%s' % (message, top.get_id_hash())
1202 old_log = patch.get_log()
1208 log = git.commit(message = msg, parents = parents,
1209 cache_update = False, tree_id = top.get_tree(),
1213 def hide_patch(self, name):
1214 """Add the patch to the hidden list.
1216 if not self.patch_exists(name):
1217 raise StackException, 'Unknown patch "%s"' % name
1218 elif self.patch_hidden(name):
1219 raise StackException, 'Patch "%s" already hidden' % name
1221 append_string(self.__hidden_file, name)
1223 def unhide_patch(self, name):
1224 """Add the patch to the hidden list.
1226 if not self.patch_exists(name):
1227 raise StackException, 'Unknown patch "%s"' % name
1228 hidden = self.get_hidden()
1229 if not name in hidden:
1230 raise StackException, 'Patch "%s" not hidden' % name
1234 f = file(self.__hidden_file, 'w+')
1235 f.writelines([line + '\n' for line in hidden])