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
22 from email.Utils import formatdate
24 from stgit.exception import *
25 from stgit.utils import *
26 from stgit.out import *
27 from stgit.run import *
28 from stgit import git, basedir, templates
29 from stgit.config import config
30 from shutil import copyfile
33 # stack exception class
34 class StackException(StgException):
39 self.should_print = True
40 def __call__(self, x, until_test, prefix):
42 self.should_print = False
44 return x[0:len(prefix)] != prefix
50 __comment_prefix = 'STG:'
51 __patch_prefix = 'STG_PATCH:'
53 def __clean_comments(f):
54 """Removes lines marked for status in a commit file
58 # remove status-prefixed lines
61 patch_filter = FilterUntil()
62 until_test = lambda t: t == (__patch_prefix + '\n')
63 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
65 # remove empty lines at the end
66 while len(lines) != 0 and lines[-1] == '\n':
69 f.seek(0); f.truncate()
72 # TODO: move this out of the stgit.stack module, it is really for
73 # higher level commands to handle the user interaction
74 def edit_file(series, line, comment, show_patch = True):
75 fname = '.stgitmsg.txt'
76 tmpl = templates.get_template('patchdescr.tmpl')
85 print >> f, __comment_prefix, comment
86 print >> f, __comment_prefix, \
87 'Lines prefixed with "%s" will be automatically removed.' \
89 print >> f, __comment_prefix, \
90 'Trailing empty lines will be automatically removed.'
93 print >> f, __patch_prefix
94 # series.get_patch(series.get_current()).get_top()
95 diff_str = git.diff(rev1 = series.get_patch(series.get_current()).get_bottom())
98 #Vim modeline must be near the end.
99 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
104 f = file(fname, 'r+')
120 """An object with stgit-like properties stored as files in a directory
122 def _set_dir(self, dir):
127 def create_empty_field(self, name):
128 create_empty_file(os.path.join(self.__dir, name))
130 def _get_field(self, name, multiline = False):
131 id_file = os.path.join(self.__dir, name)
132 if os.path.isfile(id_file):
133 line = read_string(id_file, multiline)
141 def _set_field(self, name, value, multiline = False):
142 fname = os.path.join(self.__dir, name)
143 if value and value != '':
144 write_string(fname, value, multiline)
145 elif os.path.isfile(fname):
149 class Patch(StgitObject):
150 """Basic patch implementation
152 def __init_refs(self):
153 self.__top_ref = self.__refs_base + '/' + self.__name
154 self.__log_ref = self.__top_ref + '.log'
156 def __init__(self, name, series_dir, refs_base):
157 self.__series_dir = series_dir
159 self._set_dir(os.path.join(self.__series_dir, self.__name))
160 self.__refs_base = refs_base
164 os.mkdir(self._dir())
165 self.create_empty_field('top')
167 def delete(self, keep_log = False):
168 if os.path.isdir(self._dir()):
169 for f in os.listdir(self._dir()):
170 os.remove(os.path.join(self._dir(), f))
171 os.rmdir(self._dir())
173 out.warn('Patch directory "%s" does not exist' % self._dir())
175 # the reference might not exist if the repository was corrupted
176 git.delete_ref(self.__top_ref)
177 except git.GitException, e:
179 if not keep_log and git.ref_exists(self.__log_ref):
180 git.delete_ref(self.__log_ref)
185 def rename(self, newname):
187 old_top_ref = self.__top_ref
188 old_log_ref = self.__log_ref
189 self.__name = newname
190 self._set_dir(os.path.join(self.__series_dir, self.__name))
193 git.rename_ref(old_top_ref, self.__top_ref)
194 if git.ref_exists(old_log_ref):
195 git.rename_ref(old_log_ref, self.__log_ref)
196 os.rename(olddir, self._dir())
198 def __update_top_ref(self, ref):
199 git.set_ref(self.__top_ref, ref)
201 def __update_log_ref(self, ref):
202 git.set_ref(self.__log_ref, ref)
204 def update_top_ref(self):
207 self.__update_top_ref(top)
209 def get_old_bottom(self):
210 return git.get_commit(self.get_old_top()).get_parent()
212 def get_bottom(self):
213 return git.get_commit(self.get_top()).get_parent()
215 def get_old_top(self):
216 return self._get_field('top.old')
219 top = self._get_field('top')
221 ref = git.rev_parse(self.__top_ref)
224 assert not ref or top == ref
227 def set_top(self, value, backup = False):
229 curr = self._get_field('top')
230 self._set_field('top.old', curr)
231 self._set_field('top', value)
232 self.__update_top_ref(value)
234 def restore_old_boundaries(self):
235 top = self._get_field('top.old')
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 date = self._get_field('authdate')
267 if re.match('[0-9]+\s+[+-][0-9]+', date):
268 # Unix time (seconds) + time zone
269 secs_tz = date.split()
270 date = formatdate(int(secs_tz[0]))[:-5] + secs_tz[1]
274 def set_authdate(self, date):
275 self._set_field('authdate', date or git.author().date)
277 def get_commname(self):
278 return self._get_field('commname')
280 def set_commname(self, name):
281 self._set_field('commname', name or git.committer().name)
283 def get_commemail(self):
284 return self._get_field('commemail')
286 def set_commemail(self, email):
287 self._set_field('commemail', email or git.committer().email)
290 return self._get_field('log')
292 def set_log(self, value, backup = False):
293 self._set_field('log', value)
294 self.__update_log_ref(value)
296 # The current StGIT metadata format version.
299 class PatchSet(StgitObject):
300 def __init__(self, name = None):
305 self.set_name (git.get_head_file())
306 self.__base_dir = basedir.get()
307 except git.GitException, ex:
308 raise StackException, 'GIT tree not initialised: %s' % ex
310 self._set_dir(os.path.join(self.__base_dir, 'patches', self.get_name()))
314 def set_name(self, name):
318 return self.__base_dir
321 """Return the head of the branch
323 crt = self.get_current_patch()
327 return self.get_base()
329 def get_protected(self):
330 return os.path.isfile(os.path.join(self._dir(), 'protected'))
333 protect_file = os.path.join(self._dir(), 'protected')
334 if not os.path.isfile(protect_file):
335 create_empty_file(protect_file)
338 protect_file = os.path.join(self._dir(), 'protected')
339 if os.path.isfile(protect_file):
340 os.remove(protect_file)
342 def __branch_descr(self):
343 return 'branch.%s.description' % self.get_name()
345 def get_description(self):
346 return config.get(self.__branch_descr()) or ''
348 def set_description(self, line):
350 config.set(self.__branch_descr(), line)
352 config.unset(self.__branch_descr())
354 def head_top_equal(self):
355 """Return true if the head and the top are the same
357 crt = self.get_current_patch()
359 # we don't care, no patches applied
361 return git.get_head() == crt.get_top()
363 def is_initialised(self):
364 """Checks if series is already initialised
366 return bool(config.get(self.format_version_key()))
369 def shortlog(patches):
370 log = ''.join(Run('git', 'log', '--pretty=short',
371 p.get_top(), '^%s' % p.get_bottom()).raw_output()
373 return Run('git', 'shortlog').raw_input(log).raw_output()
375 class Series(PatchSet):
376 """Class including the operations on series
378 def __init__(self, name = None):
379 """Takes a series name as the parameter.
381 PatchSet.__init__(self, name)
383 # Update the branch to the latest format version if it is
384 # initialized, but don't touch it if it isn't.
385 self.update_to_current_format_version()
387 self.__refs_base = 'refs/patches/%s' % self.get_name()
389 self.__applied_file = os.path.join(self._dir(), 'applied')
390 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
391 self.__hidden_file = os.path.join(self._dir(), 'hidden')
393 # where this series keeps its patches
394 self.__patch_dir = os.path.join(self._dir(), 'patches')
397 self.__trash_dir = os.path.join(self._dir(), 'trash')
399 def format_version_key(self):
400 return 'branch.%s.stgit.stackformatversion' % self.get_name()
402 def update_to_current_format_version(self):
403 """Update a potentially older StGIT directory structure to the
404 latest version. Note: This function should depend as little as
405 possible on external functions that may change during a format
406 version bump, since it must remain able to process older formats."""
408 branch_dir = os.path.join(self._basedir(), 'patches', self.get_name())
409 def get_format_version():
410 """Return the integer format version number, or None if the
411 branch doesn't have any StGIT metadata at all, of any version."""
412 fv = config.get(self.format_version_key())
413 ofv = config.get('branch.%s.stgitformatversion' % self.get_name())
415 # Great, there's an explicitly recorded format version
416 # number, which means that the branch is initialized and
417 # of that exact version.
420 # Old name for the version info, upgrade it
421 config.set(self.format_version_key(), ofv)
422 config.unset('branch.%s.stgitformatversion' % self.get_name())
424 elif os.path.isdir(os.path.join(branch_dir, 'patches')):
425 # There's a .git/patches/<branch>/patches dirctory, which
426 # means this is an initialized version 1 branch.
428 elif os.path.isdir(branch_dir):
429 # There's a .git/patches/<branch> directory, which means
430 # this is an initialized version 0 branch.
433 # The branch doesn't seem to be initialized at all.
435 def set_format_version(v):
436 out.info('Upgraded branch %s to format version %d' % (self.get_name(), v))
437 config.set(self.format_version_key(), '%d' % v)
439 if not os.path.isdir(d):
442 if os.path.exists(f):
445 if git.ref_exists(ref):
449 if get_format_version() == 0:
450 mkdir(os.path.join(branch_dir, 'trash'))
451 patch_dir = os.path.join(branch_dir, 'patches')
453 refs_base = 'refs/patches/%s' % self.get_name()
454 for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
455 + file(os.path.join(branch_dir, 'applied')).readlines()):
456 patch = patch.strip()
457 os.rename(os.path.join(branch_dir, patch),
458 os.path.join(patch_dir, patch))
459 Patch(patch, patch_dir, refs_base).update_top_ref()
460 set_format_version(1)
463 if get_format_version() == 1:
464 desc_file = os.path.join(branch_dir, 'description')
465 if os.path.isfile(desc_file):
466 desc = read_string(desc_file)
468 config.set('branch.%s.description' % self.get_name(), desc)
470 rm(os.path.join(branch_dir, 'current'))
471 rm_ref('refs/bases/%s' % self.get_name())
472 set_format_version(2)
474 # Make sure we're at the latest version.
475 if not get_format_version() in [None, FORMAT_VERSION]:
476 raise StackException('Branch %s is at format version %d, expected %d'
477 % (self.get_name(), get_format_version(), FORMAT_VERSION))
479 def __patch_name_valid(self, name):
480 """Raise an exception if the patch name is not valid.
482 if not name or re.search('[^\w.-]', name):
483 raise StackException, 'Invalid patch name: "%s"' % name
485 def get_patch(self, name):
486 """Return a Patch object for the given name
488 return Patch(name, self.__patch_dir, self.__refs_base)
490 def get_current_patch(self):
491 """Return a Patch object representing the topmost patch, or
492 None if there is no such patch."""
493 crt = self.get_current()
496 return self.get_patch(crt)
498 def get_current(self):
499 """Return the name of the topmost patch, or None if there is
502 applied = self.get_applied()
503 except StackException:
504 # No "applied" file: branch is not initialized.
509 # No patches applied.
512 def get_applied(self):
513 if not os.path.isfile(self.__applied_file):
514 raise StackException, 'Branch "%s" not initialised' % self.get_name()
515 return read_strings(self.__applied_file)
517 def set_applied(self, applied):
518 write_strings(self.__applied_file, applied)
520 def get_unapplied(self):
521 if not os.path.isfile(self.__unapplied_file):
522 raise StackException, 'Branch "%s" not initialised' % self.get_name()
523 return read_strings(self.__unapplied_file)
525 def set_unapplied(self, unapplied):
526 write_strings(self.__unapplied_file, unapplied)
528 def get_hidden(self):
529 if not os.path.isfile(self.__hidden_file):
531 return read_strings(self.__hidden_file)
534 # Return the parent of the bottommost patch, if there is one.
535 if os.path.isfile(self.__applied_file):
536 bottommost = file(self.__applied_file).readline().strip()
538 return self.get_patch(bottommost).get_bottom()
539 # No bottommost patch, so just return HEAD
540 return git.get_head()
542 def get_parent_remote(self):
543 value = config.get('branch.%s.remote' % self.get_name())
546 elif 'origin' in git.remotes_list():
547 out.note(('No parent remote declared for stack "%s",'
548 ' defaulting to "origin".' % self.get_name()),
549 ('Consider setting "branch.%s.remote" and'
550 ' "branch.%s.merge" with "git config".'
551 % (self.get_name(), self.get_name())))
554 raise StackException, 'Cannot find a parent remote for "%s"' % self.get_name()
556 def __set_parent_remote(self, remote):
557 value = config.set('branch.%s.remote' % self.get_name(), remote)
559 def get_parent_branch(self):
560 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
563 elif git.rev_parse('heads/origin'):
564 out.note(('No parent branch declared for stack "%s",'
565 ' defaulting to "heads/origin".' % self.get_name()),
566 ('Consider setting "branch.%s.stgit.parentbranch"'
567 ' with "git config".' % self.get_name()))
568 return 'heads/origin'
570 raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
572 def __set_parent_branch(self, name):
573 if config.get('branch.%s.remote' % self.get_name()):
574 # Never set merge if remote is not set to avoid
575 # possibly-erroneous lookups into 'origin'
576 config.set('branch.%s.merge' % self.get_name(), name)
577 config.set('branch.%s.stgit.parentbranch' % self.get_name(), name)
579 def set_parent(self, remote, localbranch):
582 self.__set_parent_remote(remote)
583 self.__set_parent_branch(localbranch)
584 # We'll enforce this later
586 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.get_name()
588 def __patch_is_current(self, patch):
589 return patch.get_name() == self.get_current()
591 def patch_applied(self, name):
592 """Return true if the patch exists in the applied list
594 return name in self.get_applied()
596 def patch_unapplied(self, name):
597 """Return true if the patch exists in the unapplied list
599 return name in self.get_unapplied()
601 def patch_hidden(self, name):
602 """Return true if the patch is hidden.
604 return name in self.get_hidden()
606 def patch_exists(self, name):
607 """Return true if there is a patch with the given name, false
609 return self.patch_applied(name) or self.patch_unapplied(name) \
610 or self.patch_hidden(name)
612 def init(self, create_at=False, parent_remote=None, parent_branch=None):
613 """Initialises the stgit series
615 if self.is_initialised():
616 raise StackException, '%s already initialized' % self.get_name()
617 for d in [self._dir()]:
618 if os.path.exists(d):
619 raise StackException, '%s already exists' % d
621 if (create_at!=False):
622 git.create_branch(self.get_name(), create_at)
624 os.makedirs(self.__patch_dir)
626 self.set_parent(parent_remote, parent_branch)
628 self.create_empty_field('applied')
629 self.create_empty_field('unapplied')
631 config.set(self.format_version_key(), str(FORMAT_VERSION))
633 def rename(self, to_name):
636 to_stack = Series(to_name)
638 if to_stack.is_initialised():
639 raise StackException, '"%s" already exists' % to_stack.get_name()
641 patches = self.get_applied() + self.get_unapplied()
643 git.rename_branch(self.get_name(), to_name)
645 for patch in patches:
646 git.rename_ref('refs/patches/%s/%s' % (self.get_name(), patch),
647 'refs/patches/%s/%s' % (to_name, patch))
648 git.rename_ref('refs/patches/%s/%s.log' % (self.get_name(), patch),
649 'refs/patches/%s/%s.log' % (to_name, patch))
650 if os.path.isdir(self._dir()):
651 rename(os.path.join(self._basedir(), 'patches'),
652 self.get_name(), to_stack.get_name())
654 # Rename the config section
655 for k in ['branch.%s', 'branch.%s.stgit']:
656 config.rename_section(k % self.get_name(), k % to_name)
658 self.__init__(to_name)
660 def clone(self, target_series):
664 # allow cloning of branches not under StGIT control
665 base = self.get_base()
667 base = git.get_head()
668 Series(target_series).init(create_at = base)
669 new_series = Series(target_series)
671 # generate an artificial description file
672 new_series.set_description('clone of "%s"' % self.get_name())
674 # clone self's entire series as unapplied patches
676 # allow cloning of branches not under StGIT control
677 applied = self.get_applied()
678 unapplied = self.get_unapplied()
679 patches = applied + unapplied
682 patches = applied = unapplied = []
684 patch = self.get_patch(p)
685 newpatch = new_series.new_patch(p, message = patch.get_description(),
686 can_edit = False, unapplied = True,
687 bottom = patch.get_bottom(),
688 top = patch.get_top(),
689 author_name = patch.get_authname(),
690 author_email = patch.get_authemail(),
691 author_date = patch.get_authdate())
693 out.info('Setting log to %s' % patch.get_log())
694 newpatch.set_log(patch.get_log())
696 out.info('No log for %s' % p)
698 # fast forward the cloned series to self's top
699 new_series.forward_patches(applied)
701 # Clone parent informations
702 value = config.get('branch.%s.remote' % self.get_name())
704 config.set('branch.%s.remote' % target_series, value)
706 value = config.get('branch.%s.merge' % self.get_name())
708 config.set('branch.%s.merge' % target_series, value)
710 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
712 config.set('branch.%s.stgit.parentbranch' % target_series, value)
714 def delete(self, force = False):
715 """Deletes an stgit series
717 if self.is_initialised():
718 patches = self.get_unapplied() + self.get_applied()
719 if not force and patches:
720 raise StackException, \
721 'Cannot delete: the series still contains patches'
723 self.get_patch(p).delete()
725 # remove the trash directory if any
726 if os.path.exists(self.__trash_dir):
727 for fname in os.listdir(self.__trash_dir):
728 os.remove(os.path.join(self.__trash_dir, fname))
729 os.rmdir(self.__trash_dir)
731 # FIXME: find a way to get rid of those manual removals
732 # (move functionality to StgitObject ?)
733 if os.path.exists(self.__applied_file):
734 os.remove(self.__applied_file)
735 if os.path.exists(self.__unapplied_file):
736 os.remove(self.__unapplied_file)
737 if os.path.exists(self.__hidden_file):
738 os.remove(self.__hidden_file)
739 if os.path.exists(self._dir()+'/orig-base'):
740 os.remove(self._dir()+'/orig-base')
742 if not os.listdir(self.__patch_dir):
743 os.rmdir(self.__patch_dir)
745 out.warn('Patch directory %s is not empty' % self.__patch_dir)
748 os.removedirs(self._dir())
750 raise StackException('Series directory %s is not empty'
754 git.delete_branch(self.get_name())
756 out.warn('Could not delete branch "%s"' % self.get_name())
758 config.remove_section('branch.%s' % self.get_name())
759 config.remove_section('branch.%s.stgit' % self.get_name())
761 def refresh_patch(self, files = None, message = None, edit = False,
764 author_name = None, author_email = None,
766 committer_name = None, committer_email = None,
767 backup = True, sign_str = None, log = 'refresh',
768 notes = None, bottom = None):
769 """Generates a new commit for the topmost patch
771 patch = self.get_current_patch()
773 raise StackException, 'No patches applied'
775 descr = patch.get_description()
776 if not (message or descr):
782 # TODO: move this out of the stgit.stack module, it is really
783 # for higher level commands to handle the user interaction
784 if not message and edit:
785 descr = edit_file(self, descr.rstrip(), \
786 'Please edit the description for patch "%s" ' \
787 'above.' % patch.get_name(), show_patch)
790 author_name = patch.get_authname()
792 author_email = patch.get_authemail()
794 author_date = patch.get_authdate()
795 if not committer_name:
796 committer_name = patch.get_commname()
797 if not committer_email:
798 committer_email = patch.get_commemail()
800 descr = add_sign_line(descr, sign_str, committer_name, committer_email)
803 bottom = patch.get_bottom()
805 commit_id = git.commit(files = files,
806 message = descr, parents = [bottom],
807 cache_update = cache_update,
809 author_name = author_name,
810 author_email = author_email,
811 author_date = author_date,
812 committer_name = committer_name,
813 committer_email = committer_email)
815 patch.set_top(commit_id, backup = backup)
816 patch.set_description(descr)
817 patch.set_authname(author_name)
818 patch.set_authemail(author_email)
819 patch.set_authdate(author_date)
820 patch.set_commname(committer_name)
821 patch.set_commemail(committer_email)
824 self.log_patch(patch, log, notes)
828 def undo_refresh(self):
829 """Undo the patch boundaries changes caused by 'refresh'
831 name = self.get_current()
834 patch = self.get_patch(name)
835 old_bottom = patch.get_old_bottom()
836 old_top = patch.get_old_top()
838 # the bottom of the patch is not changed by refresh. If the
839 # old_bottom is different, there wasn't any previous 'refresh'
840 # command (probably only a 'push')
841 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
842 raise StackException, 'No undo information available'
844 git.reset(tree_id = old_top, check_out = False)
845 if patch.restore_old_boundaries():
846 self.log_patch(patch, 'undo')
848 def new_patch(self, name, message = None, can_edit = True,
849 unapplied = False, show_patch = False,
850 top = None, bottom = None, commit = True,
851 author_name = None, author_email = None, author_date = None,
852 committer_name = None, committer_email = None,
853 before_existing = False, sign_str = None):
854 """Creates a new patch, either pointing to an existing commit object,
855 or by creating a new commit object.
858 assert commit or (top and bottom)
859 assert not before_existing or (top and bottom)
860 assert not (commit and before_existing)
861 assert (top and bottom) or (not top and not bottom)
862 assert commit or (not top or (bottom == git.get_commit(top).get_parent()))
865 self.__patch_name_valid(name)
866 if self.patch_exists(name):
867 raise StackException, 'Patch "%s" already exists' % name
869 # TODO: move this out of the stgit.stack module, it is really
870 # for higher level commands to handle the user interaction
872 return add_sign_line(msg, sign_str,
873 committer_name or git.committer().name,
874 committer_email or git.committer().email)
875 if not message and can_edit:
878 'Please enter the description for the patch above.',
881 descr = sign(message)
883 head = git.get_head()
886 name = make_patch_name(descr, self.patch_exists)
888 patch = self.get_patch(name)
891 patch.set_description(descr)
892 patch.set_authname(author_name)
893 patch.set_authemail(author_email)
894 patch.set_authdate(author_date)
895 patch.set_commname(committer_name)
896 patch.set_commemail(committer_email)
899 insert_string(self.__applied_file, patch.get_name())
901 patches = [patch.get_name()] + self.get_unapplied()
902 write_strings(self.__unapplied_file, patches)
905 append_string(self.__applied_file, patch.get_name())
910 top_commit = git.get_commit(top)
913 top_commit = git.get_commit(head)
915 # create a commit for the patch (may be empty if top == bottom);
916 # only commit on top of the current branch
917 assert(unapplied or bottom == head)
918 commit_id = git.commit(message = descr, parents = [bottom],
919 cache_update = False,
920 tree_id = top_commit.get_tree(),
921 allowempty = True, set_head = set_head,
922 author_name = author_name,
923 author_email = author_email,
924 author_date = author_date,
925 committer_name = committer_name,
926 committer_email = committer_email)
927 # set the patch top to the new commit
928 patch.set_top(commit_id)
932 self.log_patch(patch, 'new')
936 def delete_patch(self, name, keep_log = False):
939 self.__patch_name_valid(name)
940 patch = self.get_patch(name)
942 if self.__patch_is_current(patch):
944 elif self.patch_applied(name):
945 raise StackException, 'Cannot remove an applied patch, "%s", ' \
946 'which is not current' % name
947 elif not name in self.get_unapplied():
948 raise StackException, 'Unknown patch "%s"' % name
950 # save the commit id to a trash file
951 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
953 patch.delete(keep_log = keep_log)
955 unapplied = self.get_unapplied()
956 unapplied.remove(name)
957 write_strings(self.__unapplied_file, unapplied)
959 def forward_patches(self, names):
960 """Try to fast-forward an array of patches.
962 On return, patches in names[0:returned_value] have been pushed on the
963 stack. Apply the rest with push_patch
965 unapplied = self.get_unapplied()
971 assert(name in unapplied)
973 patch = self.get_patch(name)
976 bottom = patch.get_bottom()
977 top = patch.get_top()
979 # top != bottom always since we have a commit for each patch
981 # reset the backup information. No logging since the
982 # patch hasn't changed
983 patch.set_top(top, backup = True)
986 head_tree = git.get_commit(head).get_tree()
987 bottom_tree = git.get_commit(bottom).get_tree()
988 if head_tree == bottom_tree:
989 # We must just reparent this patch and create a new commit
991 descr = patch.get_description()
992 author_name = patch.get_authname()
993 author_email = patch.get_authemail()
994 author_date = patch.get_authdate()
995 committer_name = patch.get_commname()
996 committer_email = patch.get_commemail()
998 top_tree = git.get_commit(top).get_tree()
1000 top = git.commit(message = descr, parents = [head],
1001 cache_update = False,
1004 author_name = author_name,
1005 author_email = author_email,
1006 author_date = author_date,
1007 committer_name = committer_name,
1008 committer_email = committer_email)
1010 patch.set_top(top, backup = True)
1012 self.log_patch(patch, 'push(f)')
1015 # stop the fast-forwarding, must do a real merge
1019 unapplied.remove(name)
1026 append_strings(self.__applied_file, names[0:forwarded])
1027 write_strings(self.__unapplied_file, unapplied)
1031 def merged_patches(self, names):
1032 """Test which patches were merged upstream by reverse-applying
1033 them in reverse order. The function returns the list of
1034 patches detected to have been applied. The state of the tree
1035 is restored to the original one
1037 patches = [self.get_patch(name) for name in names]
1042 if git.apply_diff(p.get_top(), p.get_bottom()):
1043 merged.append(p.get_name())
1050 def push_empty_patch(self, name):
1051 """Pushes an empty patch on the stack
1053 unapplied = self.get_unapplied()
1054 assert(name in unapplied)
1056 # patch = self.get_patch(name)
1057 head = git.get_head()
1059 append_string(self.__applied_file, name)
1061 unapplied.remove(name)
1062 write_strings(self.__unapplied_file, unapplied)
1064 self.refresh_patch(bottom = head, cache_update = False, log = 'push(m)')
1066 def push_patch(self, name):
1067 """Pushes a patch on the stack
1069 unapplied = self.get_unapplied()
1070 assert(name in unapplied)
1072 patch = self.get_patch(name)
1074 head = git.get_head()
1075 bottom = patch.get_bottom()
1076 top = patch.get_top()
1077 # top != bottom always since we have a commit for each patch
1080 # A fast-forward push. Just reset the backup
1081 # information. No need for logging
1082 patch.set_top(top, backup = True)
1085 append_string(self.__applied_file, name)
1087 unapplied.remove(name)
1088 write_strings(self.__unapplied_file, unapplied)
1091 # Need to create a new commit an merge in the old patch
1095 # Try the fast applying first. If this fails, fall back to the
1097 if not git.apply_diff(bottom, top):
1098 # if git.apply_diff() fails, the patch requires a diff3
1099 # merge and can be reported as modified
1102 # merge can fail but the patch needs to be pushed
1104 git.merge(bottom, head, top, recursive = True)
1105 except git.GitException, ex:
1106 out.error('The merge failed during "push".',
1107 'Use "refresh" after fixing the conflicts or'
1108 ' revert the operation with "push --undo".')
1110 append_string(self.__applied_file, name)
1112 unapplied.remove(name)
1113 write_strings(self.__unapplied_file, unapplied)
1116 # if the merge was OK and no conflicts, just refresh the patch
1117 # The GIT cache was already updated by the merge operation
1122 self.refresh_patch(bottom = head, cache_update = False, log = log)
1124 # we store the correctly merged files only for
1125 # tracking the conflict history. Note that the
1126 # git.merge() operations should always leave the index
1127 # in a valid state (i.e. only stage 0 files)
1128 self.refresh_patch(bottom = head, cache_update = False,
1130 raise StackException, str(ex)
1134 def undo_push(self):
1135 name = self.get_current()
1138 patch = self.get_patch(name)
1139 old_bottom = patch.get_old_bottom()
1140 old_top = patch.get_old_top()
1142 # the top of the patch is changed by a push operation only
1143 # together with the bottom (otherwise the top was probably
1144 # modified by 'refresh'). If they are both unchanged, there
1145 # was a fast forward
1146 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1147 raise StackException, 'No undo information available'
1150 self.pop_patch(name)
1151 ret = patch.restore_old_boundaries()
1153 self.log_patch(patch, 'undo')
1157 def pop_patch(self, name, keep = False):
1158 """Pops the top patch from the stack
1160 applied = self.get_applied()
1162 assert(name in applied)
1164 patch = self.get_patch(name)
1166 if git.get_head_file() == self.get_name():
1167 if keep and not git.apply_diff(git.get_head(), patch.get_bottom(),
1168 check_index = False):
1169 raise StackException(
1170 'Failed to pop patches while preserving the local changes')
1171 git.switch(patch.get_bottom(), keep)
1173 git.set_branch(self.get_name(), patch.get_bottom())
1175 # save the new applied list
1176 idx = applied.index(name) + 1
1178 popped = applied[:idx]
1180 unapplied = popped + self.get_unapplied()
1181 write_strings(self.__unapplied_file, unapplied)
1185 write_strings(self.__applied_file, applied)
1187 def empty_patch(self, name):
1188 """Returns True if the patch is empty
1190 self.__patch_name_valid(name)
1191 patch = self.get_patch(name)
1192 bottom = patch.get_bottom()
1193 top = patch.get_top()
1197 elif git.get_commit(top).get_tree() \
1198 == git.get_commit(bottom).get_tree():
1203 def rename_patch(self, oldname, newname):
1204 self.__patch_name_valid(newname)
1206 applied = self.get_applied()
1207 unapplied = self.get_unapplied()
1209 if oldname == newname:
1210 raise StackException, '"To" name and "from" name are the same'
1212 if newname in applied or newname in unapplied:
1213 raise StackException, 'Patch "%s" already exists' % newname
1215 if oldname in unapplied:
1216 self.get_patch(oldname).rename(newname)
1217 unapplied[unapplied.index(oldname)] = newname
1218 write_strings(self.__unapplied_file, unapplied)
1219 elif oldname in applied:
1220 self.get_patch(oldname).rename(newname)
1222 applied[applied.index(oldname)] = newname
1223 write_strings(self.__applied_file, applied)
1225 raise StackException, 'Unknown patch "%s"' % oldname
1227 def log_patch(self, patch, message, notes = None):
1228 """Generate a log commit for a patch
1230 top = git.get_commit(patch.get_top())
1231 old_log = patch.get_log()
1234 # replace the current log entry
1236 raise StackException, \
1237 'No log entry to annotate for patch "%s"' \
1240 log_commit = git.get_commit(old_log)
1241 msg = log_commit.get_log().split('\n')[0]
1242 log_parent = log_commit.get_parent()
1244 parents = [log_parent]
1248 # generate a new log entry
1250 msg = '%s\t%s' % (message, top.get_id_hash())
1257 msg += '\n\n' + notes
1259 log = git.commit(message = msg, parents = parents,
1260 cache_update = False, tree_id = top.get_tree(),
1264 def hide_patch(self, name):
1265 """Add the patch to the hidden list.
1267 unapplied = self.get_unapplied()
1268 if name not in unapplied:
1269 # keep the checking order for backward compatibility with
1270 # the old hidden patches functionality
1271 if self.patch_applied(name):
1272 raise StackException, 'Cannot hide applied patch "%s"' % name
1273 elif self.patch_hidden(name):
1274 raise StackException, 'Patch "%s" already hidden' % name
1276 raise StackException, 'Unknown patch "%s"' % name
1278 if not self.patch_hidden(name):
1279 # check needed for backward compatibility with the old
1280 # hidden patches functionality
1281 append_string(self.__hidden_file, name)
1283 unapplied.remove(name)
1284 write_strings(self.__unapplied_file, unapplied)
1286 def unhide_patch(self, name):
1287 """Remove the patch from the hidden list.
1289 hidden = self.get_hidden()
1290 if not name in hidden:
1291 if self.patch_applied(name) or self.patch_unapplied(name):
1292 raise StackException, 'Patch "%s" not hidden' % name
1294 raise StackException, 'Unknown patch "%s"' % name
1297 write_strings(self.__hidden_file, hidden)
1299 if not self.patch_applied(name) and not self.patch_unapplied(name):
1300 # check needed for backward compatibility with the old
1301 # hidden patches functionality
1302 append_string(self.__unapplied_file, name)