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())
166 def delete(self, keep_log = False):
167 if os.path.isdir(self._dir()):
168 for f in os.listdir(self._dir()):
169 os.remove(os.path.join(self._dir(), f))
170 os.rmdir(self._dir())
172 out.warn('Patch directory "%s" does not exist' % self._dir())
174 # the reference might not exist if the repository was corrupted
175 git.delete_ref(self.__top_ref)
176 except git.GitException, e:
178 if not keep_log and git.ref_exists(self.__log_ref):
179 git.delete_ref(self.__log_ref)
184 def rename(self, newname):
186 old_top_ref = self.__top_ref
187 old_log_ref = self.__log_ref
188 self.__name = newname
189 self._set_dir(os.path.join(self.__series_dir, self.__name))
192 git.rename_ref(old_top_ref, self.__top_ref)
193 if git.ref_exists(old_log_ref):
194 git.rename_ref(old_log_ref, self.__log_ref)
195 os.rename(olddir, self._dir())
197 def __update_top_ref(self, ref):
198 git.set_ref(self.__top_ref, ref)
199 self._set_field('top', ref)
200 self._set_field('bottom', git.get_commit(ref).get_parent())
202 def __update_log_ref(self, ref):
203 git.set_ref(self.__log_ref, ref)
205 def get_old_bottom(self):
206 return git.get_commit(self.get_old_top()).get_parent()
208 def get_bottom(self):
209 return git.get_commit(self.get_top()).get_parent()
211 def get_old_top(self):
212 return self._get_field('top.old')
215 return git.rev_parse(self.__top_ref)
217 def set_top(self, value, backup = False):
219 curr_top = self.get_top()
220 self._set_field('top.old', curr_top)
221 self._set_field('bottom.old', git.get_commit(curr_top).get_parent())
222 self.__update_top_ref(value)
224 def restore_old_boundaries(self):
225 top = self._get_field('top.old')
228 self.__update_top_ref(top)
233 def get_description(self):
234 return self._get_field('description', True)
236 def set_description(self, line):
237 self._set_field('description', line, True)
239 def get_authname(self):
240 return self._get_field('authname')
242 def set_authname(self, name):
243 self._set_field('authname', name or git.author().name)
245 def get_authemail(self):
246 return self._get_field('authemail')
248 def set_authemail(self, email):
249 self._set_field('authemail', email or git.author().email)
251 def get_authdate(self):
252 date = self._get_field('authdate')
256 if re.match('[0-9]+\s+[+-][0-9]+', date):
257 # Unix time (seconds) + time zone
258 secs_tz = date.split()
259 date = formatdate(int(secs_tz[0]))[:-5] + secs_tz[1]
263 def set_authdate(self, date):
264 self._set_field('authdate', date or git.author().date)
266 def get_commname(self):
267 return self._get_field('commname')
269 def set_commname(self, name):
270 self._set_field('commname', name or git.committer().name)
272 def get_commemail(self):
273 return self._get_field('commemail')
275 def set_commemail(self, email):
276 self._set_field('commemail', email or git.committer().email)
279 return self._get_field('log')
281 def set_log(self, value, backup = False):
282 self._set_field('log', value)
283 self.__update_log_ref(value)
285 # The current StGIT metadata format version.
288 class PatchSet(StgitObject):
289 def __init__(self, name = None):
294 self.set_name (git.get_head_file())
295 self.__base_dir = basedir.get()
296 except git.GitException, ex:
297 raise StackException, 'GIT tree not initialised: %s' % ex
299 self._set_dir(os.path.join(self.__base_dir, 'patches', self.get_name()))
303 def set_name(self, name):
307 return self.__base_dir
310 """Return the head of the branch
312 crt = self.get_current_patch()
316 return self.get_base()
318 def get_protected(self):
319 return os.path.isfile(os.path.join(self._dir(), 'protected'))
322 protect_file = os.path.join(self._dir(), 'protected')
323 if not os.path.isfile(protect_file):
324 create_empty_file(protect_file)
327 protect_file = os.path.join(self._dir(), 'protected')
328 if os.path.isfile(protect_file):
329 os.remove(protect_file)
331 def __branch_descr(self):
332 return 'branch.%s.description' % self.get_name()
334 def get_description(self):
335 return config.get(self.__branch_descr()) or ''
337 def set_description(self, line):
339 config.set(self.__branch_descr(), line)
341 config.unset(self.__branch_descr())
343 def head_top_equal(self):
344 """Return true if the head and the top are the same
346 crt = self.get_current_patch()
348 # we don't care, no patches applied
350 return git.get_head() == crt.get_top()
352 def is_initialised(self):
353 """Checks if series is already initialised
355 return bool(config.get(self.format_version_key()))
358 def shortlog(patches):
359 log = ''.join(Run('git', 'log', '--pretty=short',
360 p.get_top(), '^%s' % p.get_bottom()).raw_output()
362 return Run('git', 'shortlog').raw_input(log).raw_output()
364 class Series(PatchSet):
365 """Class including the operations on series
367 def __init__(self, name = None):
368 """Takes a series name as the parameter.
370 PatchSet.__init__(self, name)
372 # Update the branch to the latest format version if it is
373 # initialized, but don't touch it if it isn't.
374 self.update_to_current_format_version()
376 self.__refs_base = 'refs/patches/%s' % self.get_name()
378 self.__applied_file = os.path.join(self._dir(), 'applied')
379 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
380 self.__hidden_file = os.path.join(self._dir(), 'hidden')
382 # where this series keeps its patches
383 self.__patch_dir = os.path.join(self._dir(), 'patches')
386 self.__trash_dir = os.path.join(self._dir(), 'trash')
388 def format_version_key(self):
389 return 'branch.%s.stgit.stackformatversion' % self.get_name()
391 def update_to_current_format_version(self):
392 """Update a potentially older StGIT directory structure to the
393 latest version. Note: This function should depend as little as
394 possible on external functions that may change during a format
395 version bump, since it must remain able to process older formats."""
397 branch_dir = os.path.join(self._basedir(), 'patches', self.get_name())
398 def get_format_version():
399 """Return the integer format version number, or None if the
400 branch doesn't have any StGIT metadata at all, of any version."""
401 fv = config.get(self.format_version_key())
402 ofv = config.get('branch.%s.stgitformatversion' % self.get_name())
404 # Great, there's an explicitly recorded format version
405 # number, which means that the branch is initialized and
406 # of that exact version.
409 # Old name for the version info, upgrade it
410 config.set(self.format_version_key(), ofv)
411 config.unset('branch.%s.stgitformatversion' % self.get_name())
413 elif os.path.isdir(os.path.join(branch_dir, 'patches')):
414 # There's a .git/patches/<branch>/patches dirctory, which
415 # means this is an initialized version 1 branch.
417 elif os.path.isdir(branch_dir):
418 # There's a .git/patches/<branch> directory, which means
419 # this is an initialized version 0 branch.
422 # The branch doesn't seem to be initialized at all.
424 def set_format_version(v):
425 out.info('Upgraded branch %s to format version %d' % (self.get_name(), v))
426 config.set(self.format_version_key(), '%d' % v)
428 if not os.path.isdir(d):
431 if os.path.exists(f):
434 if git.ref_exists(ref):
438 if get_format_version() == 0:
439 mkdir(os.path.join(branch_dir, 'trash'))
440 patch_dir = os.path.join(branch_dir, 'patches')
442 refs_base = 'refs/patches/%s' % self.get_name()
443 for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
444 + file(os.path.join(branch_dir, 'applied')).readlines()):
445 patch = patch.strip()
446 os.rename(os.path.join(branch_dir, patch),
447 os.path.join(patch_dir, patch))
448 topfield = os.path.join(patch_dir, patch, 'top')
449 if os.path.isfile(topfield):
450 top = read_string(topfield, False)
454 git.set_ref(refs_base + '/' + patch, top)
455 set_format_version(1)
458 if get_format_version() == 1:
459 desc_file = os.path.join(branch_dir, 'description')
460 if os.path.isfile(desc_file):
461 desc = read_string(desc_file)
463 config.set('branch.%s.description' % self.get_name(), desc)
465 rm(os.path.join(branch_dir, 'current'))
466 rm_ref('refs/bases/%s' % self.get_name())
467 set_format_version(2)
469 # Make sure we're at the latest version.
470 if not get_format_version() in [None, FORMAT_VERSION]:
471 raise StackException('Branch %s is at format version %d, expected %d'
472 % (self.get_name(), get_format_version(), FORMAT_VERSION))
474 def __patch_name_valid(self, name):
475 """Raise an exception if the patch name is not valid.
477 if not name or re.search('[^\w.-]', name):
478 raise StackException, 'Invalid patch name: "%s"' % name
480 def get_patch(self, name):
481 """Return a Patch object for the given name
483 return Patch(name, self.__patch_dir, self.__refs_base)
485 def get_current_patch(self):
486 """Return a Patch object representing the topmost patch, or
487 None if there is no such patch."""
488 crt = self.get_current()
491 return self.get_patch(crt)
493 def get_current(self):
494 """Return the name of the topmost patch, or None if there is
497 applied = self.get_applied()
498 except StackException:
499 # No "applied" file: branch is not initialized.
504 # No patches applied.
507 def get_applied(self):
508 if not os.path.isfile(self.__applied_file):
509 raise StackException, 'Branch "%s" not initialised' % self.get_name()
510 return read_strings(self.__applied_file)
512 def set_applied(self, applied):
513 write_strings(self.__applied_file, applied)
515 def get_unapplied(self):
516 if not os.path.isfile(self.__unapplied_file):
517 raise StackException, 'Branch "%s" not initialised' % self.get_name()
518 return read_strings(self.__unapplied_file)
520 def set_unapplied(self, unapplied):
521 write_strings(self.__unapplied_file, unapplied)
523 def get_hidden(self):
524 if not os.path.isfile(self.__hidden_file):
526 return read_strings(self.__hidden_file)
529 # Return the parent of the bottommost patch, if there is one.
530 if os.path.isfile(self.__applied_file):
531 bottommost = file(self.__applied_file).readline().strip()
533 return self.get_patch(bottommost).get_bottom()
534 # No bottommost patch, so just return HEAD
535 return git.get_head()
537 def get_parent_remote(self):
538 value = config.get('branch.%s.remote' % self.get_name())
541 elif 'origin' in git.remotes_list():
542 out.note(('No parent remote declared for stack "%s",'
543 ' defaulting to "origin".' % self.get_name()),
544 ('Consider setting "branch.%s.remote" and'
545 ' "branch.%s.merge" with "git config".'
546 % (self.get_name(), self.get_name())))
549 raise StackException, 'Cannot find a parent remote for "%s"' % self.get_name()
551 def __set_parent_remote(self, remote):
552 value = config.set('branch.%s.remote' % self.get_name(), remote)
554 def get_parent_branch(self):
555 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
558 elif git.rev_parse('heads/origin'):
559 out.note(('No parent branch declared for stack "%s",'
560 ' defaulting to "heads/origin".' % self.get_name()),
561 ('Consider setting "branch.%s.stgit.parentbranch"'
562 ' with "git config".' % self.get_name()))
563 return 'heads/origin'
565 raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
567 def __set_parent_branch(self, name):
568 if config.get('branch.%s.remote' % self.get_name()):
569 # Never set merge if remote is not set to avoid
570 # possibly-erroneous lookups into 'origin'
571 config.set('branch.%s.merge' % self.get_name(), name)
572 config.set('branch.%s.stgit.parentbranch' % self.get_name(), name)
574 def set_parent(self, remote, localbranch):
577 self.__set_parent_remote(remote)
578 self.__set_parent_branch(localbranch)
579 # We'll enforce this later
581 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.get_name()
583 def __patch_is_current(self, patch):
584 return patch.get_name() == self.get_current()
586 def patch_applied(self, name):
587 """Return true if the patch exists in the applied list
589 return name in self.get_applied()
591 def patch_unapplied(self, name):
592 """Return true if the patch exists in the unapplied list
594 return name in self.get_unapplied()
596 def patch_hidden(self, name):
597 """Return true if the patch is hidden.
599 return name in self.get_hidden()
601 def patch_exists(self, name):
602 """Return true if there is a patch with the given name, false
604 return self.patch_applied(name) or self.patch_unapplied(name) \
605 or self.patch_hidden(name)
607 def init(self, create_at=False, parent_remote=None, parent_branch=None):
608 """Initialises the stgit series
610 if self.is_initialised():
611 raise StackException, '%s already initialized' % self.get_name()
612 for d in [self._dir()]:
613 if os.path.exists(d):
614 raise StackException, '%s already exists' % d
616 if (create_at!=False):
617 git.create_branch(self.get_name(), create_at)
619 os.makedirs(self.__patch_dir)
621 self.set_parent(parent_remote, parent_branch)
623 self.create_empty_field('applied')
624 self.create_empty_field('unapplied')
626 config.set(self.format_version_key(), str(FORMAT_VERSION))
628 def rename(self, to_name):
631 to_stack = Series(to_name)
633 if to_stack.is_initialised():
634 raise StackException, '"%s" already exists' % to_stack.get_name()
636 patches = self.get_applied() + self.get_unapplied()
638 git.rename_branch(self.get_name(), to_name)
640 for patch in patches:
641 git.rename_ref('refs/patches/%s/%s' % (self.get_name(), patch),
642 'refs/patches/%s/%s' % (to_name, patch))
643 git.rename_ref('refs/patches/%s/%s.log' % (self.get_name(), patch),
644 'refs/patches/%s/%s.log' % (to_name, patch))
645 if os.path.isdir(self._dir()):
646 rename(os.path.join(self._basedir(), 'patches'),
647 self.get_name(), to_stack.get_name())
649 # Rename the config section
650 for k in ['branch.%s', 'branch.%s.stgit']:
651 config.rename_section(k % self.get_name(), k % to_name)
653 self.__init__(to_name)
655 def clone(self, target_series):
659 # allow cloning of branches not under StGIT control
660 base = self.get_base()
662 base = git.get_head()
663 Series(target_series).init(create_at = base)
664 new_series = Series(target_series)
666 # generate an artificial description file
667 new_series.set_description('clone of "%s"' % self.get_name())
669 # clone self's entire series as unapplied patches
671 # allow cloning of branches not under StGIT control
672 applied = self.get_applied()
673 unapplied = self.get_unapplied()
674 patches = applied + unapplied
677 patches = applied = unapplied = []
679 patch = self.get_patch(p)
680 newpatch = new_series.new_patch(p, message = patch.get_description(),
681 can_edit = False, unapplied = True,
682 bottom = patch.get_bottom(),
683 top = patch.get_top(),
684 author_name = patch.get_authname(),
685 author_email = patch.get_authemail(),
686 author_date = patch.get_authdate())
688 out.info('Setting log to %s' % patch.get_log())
689 newpatch.set_log(patch.get_log())
691 out.info('No log for %s' % p)
693 # fast forward the cloned series to self's top
694 new_series.forward_patches(applied)
696 # Clone parent informations
697 value = config.get('branch.%s.remote' % self.get_name())
699 config.set('branch.%s.remote' % target_series, value)
701 value = config.get('branch.%s.merge' % self.get_name())
703 config.set('branch.%s.merge' % target_series, value)
705 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
707 config.set('branch.%s.stgit.parentbranch' % target_series, value)
709 def delete(self, force = False):
710 """Deletes an stgit series
712 if self.is_initialised():
713 patches = self.get_unapplied() + self.get_applied()
714 if not force and patches:
715 raise StackException, \
716 'Cannot delete: the series still contains patches'
718 self.get_patch(p).delete()
720 # remove the trash directory if any
721 if os.path.exists(self.__trash_dir):
722 for fname in os.listdir(self.__trash_dir):
723 os.remove(os.path.join(self.__trash_dir, fname))
724 os.rmdir(self.__trash_dir)
726 # FIXME: find a way to get rid of those manual removals
727 # (move functionality to StgitObject ?)
728 if os.path.exists(self.__applied_file):
729 os.remove(self.__applied_file)
730 if os.path.exists(self.__unapplied_file):
731 os.remove(self.__unapplied_file)
732 if os.path.exists(self.__hidden_file):
733 os.remove(self.__hidden_file)
734 if os.path.exists(self._dir()+'/orig-base'):
735 os.remove(self._dir()+'/orig-base')
737 if not os.listdir(self.__patch_dir):
738 os.rmdir(self.__patch_dir)
740 out.warn('Patch directory %s is not empty' % self.__patch_dir)
743 os.removedirs(self._dir())
745 raise StackException('Series directory %s is not empty'
749 git.delete_branch(self.get_name())
751 out.warn('Could not delete branch "%s"' % self.get_name())
753 config.remove_section('branch.%s' % self.get_name())
754 config.remove_section('branch.%s.stgit' % self.get_name())
756 def refresh_patch(self, files = None, message = None, edit = False,
760 author_name = None, author_email = None,
762 committer_name = None, committer_email = None,
763 backup = True, sign_str = None, log = 'refresh',
764 notes = None, bottom = None):
765 """Generates a new commit for the topmost patch
767 patch = self.get_current_patch()
769 raise StackException, 'No patches applied'
771 descr = patch.get_description()
772 if not (message or descr):
778 # TODO: move this out of the stgit.stack module, it is really
779 # for higher level commands to handle the user interaction
780 if not message and edit:
781 descr = edit_file(self, descr.rstrip(), \
782 'Please edit the description for patch "%s" ' \
783 'above.' % patch.get_name(), show_patch)
786 author_name = patch.get_authname()
788 author_email = patch.get_authemail()
790 author_date = patch.get_authdate()
791 if not committer_name:
792 committer_name = patch.get_commname()
793 if not committer_email:
794 committer_email = patch.get_commemail()
796 descr = add_sign_line(descr, sign_str, committer_name, committer_email)
799 bottom = patch.get_bottom()
802 tree_id = git.get_commit(bottom).get_tree()
806 commit_id = git.commit(files = files,
807 message = descr, parents = [bottom],
808 cache_update = cache_update,
812 author_name = author_name,
813 author_email = author_email,
814 author_date = author_date,
815 committer_name = committer_name,
816 committer_email = committer_email)
818 patch.set_top(commit_id, backup = backup)
819 patch.set_description(descr)
820 patch.set_authname(author_name)
821 patch.set_authemail(author_email)
822 patch.set_authdate(author_date)
823 patch.set_commname(committer_name)
824 patch.set_commemail(committer_email)
827 self.log_patch(patch, log, notes)
831 def undo_refresh(self):
832 """Undo the patch boundaries changes caused by 'refresh'
834 name = self.get_current()
837 patch = self.get_patch(name)
838 old_bottom = patch.get_old_bottom()
839 old_top = patch.get_old_top()
841 # the bottom of the patch is not changed by refresh. If the
842 # old_bottom is different, there wasn't any previous 'refresh'
843 # command (probably only a 'push')
844 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
845 raise StackException, 'No undo information available'
847 git.reset(tree_id = old_top, check_out = False)
848 if patch.restore_old_boundaries():
849 self.log_patch(patch, 'undo')
851 def new_patch(self, name, message = None, can_edit = True,
852 unapplied = False, show_patch = False,
853 top = None, bottom = None, commit = True,
854 author_name = None, author_email = None, author_date = None,
855 committer_name = None, committer_email = None,
856 before_existing = False, sign_str = None):
857 """Creates a new patch, either pointing to an existing commit object,
858 or by creating a new commit object.
861 assert commit or (top and bottom)
862 assert not before_existing or (top and bottom)
863 assert not (commit and before_existing)
864 assert (top and bottom) or (not top and not bottom)
865 assert commit or (not top or (bottom == git.get_commit(top).get_parent()))
868 self.__patch_name_valid(name)
869 if self.patch_exists(name):
870 raise StackException, 'Patch "%s" already exists' % name
872 # TODO: move this out of the stgit.stack module, it is really
873 # for higher level commands to handle the user interaction
875 return add_sign_line(msg, sign_str,
876 committer_name or git.committer().name,
877 committer_email or git.committer().email)
878 if not message and can_edit:
881 'Please enter the description for the patch above.',
884 descr = sign(message)
886 head = git.get_head()
889 name = make_patch_name(descr, self.patch_exists)
891 patch = self.get_patch(name)
894 patch.set_description(descr)
895 patch.set_authname(author_name)
896 patch.set_authemail(author_email)
897 patch.set_authdate(author_date)
898 patch.set_commname(committer_name)
899 patch.set_commemail(committer_email)
902 insert_string(self.__applied_file, patch.get_name())
904 patches = [patch.get_name()] + self.get_unapplied()
905 write_strings(self.__unapplied_file, patches)
908 append_string(self.__applied_file, patch.get_name())
913 top_commit = git.get_commit(top)
916 top_commit = git.get_commit(head)
918 # create a commit for the patch (may be empty if top == bottom);
919 # only commit on top of the current branch
920 assert(unapplied or bottom == head)
921 commit_id = git.commit(message = descr, parents = [bottom],
922 cache_update = False,
923 tree_id = top_commit.get_tree(),
924 allowempty = True, set_head = set_head,
925 author_name = author_name,
926 author_email = author_email,
927 author_date = author_date,
928 committer_name = committer_name,
929 committer_email = committer_email)
930 # set the patch top to the new commit
931 patch.set_top(commit_id)
935 self.log_patch(patch, 'new')
939 def delete_patch(self, name, keep_log = False):
942 self.__patch_name_valid(name)
943 patch = self.get_patch(name)
945 if self.__patch_is_current(patch):
947 elif self.patch_applied(name):
948 raise StackException, 'Cannot remove an applied patch, "%s", ' \
949 'which is not current' % name
950 elif not name in self.get_unapplied():
951 raise StackException, 'Unknown patch "%s"' % name
953 # save the commit id to a trash file
954 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
956 patch.delete(keep_log = keep_log)
958 unapplied = self.get_unapplied()
959 unapplied.remove(name)
960 write_strings(self.__unapplied_file, unapplied)
962 def forward_patches(self, names):
963 """Try to fast-forward an array of patches.
965 On return, patches in names[0:returned_value] have been pushed on the
966 stack. Apply the rest with push_patch
968 unapplied = self.get_unapplied()
974 assert(name in unapplied)
976 patch = self.get_patch(name)
979 bottom = patch.get_bottom()
980 top = patch.get_top()
982 # top != bottom always since we have a commit for each patch
984 # reset the backup information. No logging since the
985 # patch hasn't changed
986 patch.set_top(top, backup = True)
989 head_tree = git.get_commit(head).get_tree()
990 bottom_tree = git.get_commit(bottom).get_tree()
991 if head_tree == bottom_tree:
992 # We must just reparent this patch and create a new commit
994 descr = patch.get_description()
995 author_name = patch.get_authname()
996 author_email = patch.get_authemail()
997 author_date = patch.get_authdate()
998 committer_name = patch.get_commname()
999 committer_email = patch.get_commemail()
1001 top_tree = git.get_commit(top).get_tree()
1003 top = git.commit(message = descr, parents = [head],
1004 cache_update = False,
1007 author_name = author_name,
1008 author_email = author_email,
1009 author_date = author_date,
1010 committer_name = committer_name,
1011 committer_email = committer_email)
1013 patch.set_top(top, backup = True)
1015 self.log_patch(patch, 'push(f)')
1018 # stop the fast-forwarding, must do a real merge
1022 unapplied.remove(name)
1029 append_strings(self.__applied_file, names[0:forwarded])
1030 write_strings(self.__unapplied_file, unapplied)
1034 def merged_patches(self, names):
1035 """Test which patches were merged upstream by reverse-applying
1036 them in reverse order. The function returns the list of
1037 patches detected to have been applied. The state of the tree
1038 is restored to the original one
1040 patches = [self.get_patch(name) for name in names]
1045 if git.apply_diff(p.get_top(), p.get_bottom()):
1046 merged.append(p.get_name())
1053 def push_empty_patch(self, name):
1054 """Pushes an empty patch on the stack
1056 unapplied = self.get_unapplied()
1057 assert(name in unapplied)
1059 # patch = self.get_patch(name)
1060 head = git.get_head()
1062 append_string(self.__applied_file, name)
1064 unapplied.remove(name)
1065 write_strings(self.__unapplied_file, unapplied)
1067 self.refresh_patch(bottom = head, cache_update = False, log = 'push(m)')
1069 def push_patch(self, name):
1070 """Pushes a patch on the stack
1072 unapplied = self.get_unapplied()
1073 assert(name in unapplied)
1075 patch = self.get_patch(name)
1077 head = git.get_head()
1078 bottom = patch.get_bottom()
1079 top = patch.get_top()
1080 # top != bottom always since we have a commit for each patch
1083 # A fast-forward push. Just reset the backup
1084 # information. No need for logging
1085 patch.set_top(top, backup = True)
1088 append_string(self.__applied_file, name)
1090 unapplied.remove(name)
1091 write_strings(self.__unapplied_file, unapplied)
1094 # Need to create a new commit an merge in the old patch
1098 # Try the fast applying first. If this fails, fall back to the
1100 if not git.apply_diff(bottom, top):
1101 # if git.apply_diff() fails, the patch requires a diff3
1102 # merge and can be reported as modified
1105 # merge can fail but the patch needs to be pushed
1107 git.merge_recursive(bottom, head, top)
1108 except git.GitConflictException, ex:
1110 except git.GitException, ex:
1111 out.error('The merge failed during "push".',
1112 'Use "refresh" after fixing the conflicts or'
1113 ' revert the operation with "push --undo".')
1115 append_string(self.__applied_file, name)
1117 unapplied.remove(name)
1118 write_strings(self.__unapplied_file, unapplied)
1121 # if the merge was OK and no conflicts, just refresh the patch
1122 # The GIT cache was already updated by the merge operation
1127 self.refresh_patch(bottom = head, cache_update = False, log = log)
1129 # we make the patch empty, with the merged state in the
1131 self.refresh_patch(bottom = head, cache_update = False,
1132 empty = True, log = 'push(c)')
1133 raise StackException, str(ex)
1137 def undo_push(self):
1138 name = self.get_current()
1141 patch = self.get_patch(name)
1142 old_bottom = patch.get_old_bottom()
1143 old_top = patch.get_old_top()
1145 # the top of the patch is changed by a push operation only
1146 # together with the bottom (otherwise the top was probably
1147 # modified by 'refresh'). If they are both unchanged, there
1148 # was a fast forward
1149 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1150 raise StackException, 'No undo information available'
1153 self.pop_patch(name)
1154 ret = patch.restore_old_boundaries()
1156 self.log_patch(patch, 'undo')
1160 def pop_patch(self, name, keep = False):
1161 """Pops the top patch from the stack
1163 applied = self.get_applied()
1165 assert(name in applied)
1167 patch = self.get_patch(name)
1169 if git.get_head_file() == self.get_name():
1170 if keep and not git.apply_diff(git.get_head(), patch.get_bottom(),
1171 check_index = False):
1172 raise StackException(
1173 'Failed to pop patches while preserving the local changes')
1174 git.switch(patch.get_bottom(), keep)
1176 git.set_branch(self.get_name(), patch.get_bottom())
1178 # save the new applied list
1179 idx = applied.index(name) + 1
1181 popped = applied[:idx]
1183 unapplied = popped + self.get_unapplied()
1184 write_strings(self.__unapplied_file, unapplied)
1188 write_strings(self.__applied_file, applied)
1190 def empty_patch(self, name):
1191 """Returns True if the patch is empty
1193 self.__patch_name_valid(name)
1194 patch = self.get_patch(name)
1195 bottom = patch.get_bottom()
1196 top = patch.get_top()
1200 elif git.get_commit(top).get_tree() \
1201 == git.get_commit(bottom).get_tree():
1206 def rename_patch(self, oldname, newname):
1207 self.__patch_name_valid(newname)
1209 applied = self.get_applied()
1210 unapplied = self.get_unapplied()
1212 if oldname == newname:
1213 raise StackException, '"To" name and "from" name are the same'
1215 if newname in applied or newname in unapplied:
1216 raise StackException, 'Patch "%s" already exists' % newname
1218 if oldname in unapplied:
1219 self.get_patch(oldname).rename(newname)
1220 unapplied[unapplied.index(oldname)] = newname
1221 write_strings(self.__unapplied_file, unapplied)
1222 elif oldname in applied:
1223 self.get_patch(oldname).rename(newname)
1225 applied[applied.index(oldname)] = newname
1226 write_strings(self.__applied_file, applied)
1228 raise StackException, 'Unknown patch "%s"' % oldname
1230 def log_patch(self, patch, message, notes = None):
1231 """Generate a log commit for a patch
1233 top = git.get_commit(patch.get_top())
1234 old_log = patch.get_log()
1237 # replace the current log entry
1239 raise StackException, \
1240 'No log entry to annotate for patch "%s"' \
1243 log_commit = git.get_commit(old_log)
1244 msg = log_commit.get_log().split('\n')[0]
1245 log_parent = log_commit.get_parent()
1247 parents = [log_parent]
1251 # generate a new log entry
1253 msg = '%s\t%s' % (message, top.get_id_hash())
1260 msg += '\n\n' + notes
1262 log = git.commit(message = msg, parents = parents,
1263 cache_update = False, tree_id = top.get_tree(),
1267 def hide_patch(self, name):
1268 """Add the patch to the hidden list.
1270 unapplied = self.get_unapplied()
1271 if name not in unapplied:
1272 # keep the checking order for backward compatibility with
1273 # the old hidden patches functionality
1274 if self.patch_applied(name):
1275 raise StackException, 'Cannot hide applied patch "%s"' % name
1276 elif self.patch_hidden(name):
1277 raise StackException, 'Patch "%s" already hidden' % name
1279 raise StackException, 'Unknown patch "%s"' % name
1281 if not self.patch_hidden(name):
1282 # check needed for backward compatibility with the old
1283 # hidden patches functionality
1284 append_string(self.__hidden_file, name)
1286 unapplied.remove(name)
1287 write_strings(self.__unapplied_file, unapplied)
1289 def unhide_patch(self, name):
1290 """Remove the patch from the hidden list.
1292 hidden = self.get_hidden()
1293 if not name in hidden:
1294 if self.patch_applied(name) or self.patch_unapplied(name):
1295 raise StackException, 'Patch "%s" not hidden' % name
1297 raise StackException, 'Unknown patch "%s"' % name
1300 write_strings(self.__hidden_file, hidden)
1302 if not self.patch_applied(name) and not self.patch_unapplied(name):
1303 # check needed for backward compatibility with the old
1304 # hidden patches functionality
1305 append_string(self.__unapplied_file, name)