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
31 from stgit.lib import git as libgit, stackupgrade
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 class PatchSet(StgitObject):
286 def __init__(self, name = None):
291 self.set_name (git.get_head_file())
292 self.__base_dir = basedir.get()
293 except git.GitException, ex:
294 raise StackException, 'GIT tree not initialised: %s' % ex
296 self._set_dir(os.path.join(self.__base_dir, 'patches', self.get_name()))
300 def set_name(self, name):
304 return self.__base_dir
307 """Return the head of the branch
309 crt = self.get_current_patch()
313 return self.get_base()
315 def get_protected(self):
316 return os.path.isfile(os.path.join(self._dir(), 'protected'))
319 protect_file = os.path.join(self._dir(), 'protected')
320 if not os.path.isfile(protect_file):
321 create_empty_file(protect_file)
324 protect_file = os.path.join(self._dir(), 'protected')
325 if os.path.isfile(protect_file):
326 os.remove(protect_file)
328 def __branch_descr(self):
329 return 'branch.%s.description' % self.get_name()
331 def get_description(self):
332 return config.get(self.__branch_descr()) or ''
334 def set_description(self, line):
336 config.set(self.__branch_descr(), line)
338 config.unset(self.__branch_descr())
340 def head_top_equal(self):
341 """Return true if the head and the top are the same
343 crt = self.get_current_patch()
345 # we don't care, no patches applied
347 return git.get_head() == crt.get_top()
349 def is_initialised(self):
350 """Checks if series is already initialised
352 return config.get(stackupgrade.format_version_key(self.get_name())
356 def shortlog(patches):
357 log = ''.join(Run('git', 'log', '--pretty=short',
358 p.get_top(), '^%s' % p.get_bottom()).raw_output()
360 return Run('git', 'shortlog').raw_input(log).raw_output()
362 class Series(PatchSet):
363 """Class including the operations on series
365 def __init__(self, name = None):
366 """Takes a series name as the parameter.
368 PatchSet.__init__(self, name)
370 # Update the branch to the latest format version if it is
371 # initialized, but don't touch it if it isn't.
372 stackupgrade.update_to_current_format_version(
373 libgit.Repository.default(), self.get_name())
375 self.__refs_base = 'refs/patches/%s' % self.get_name()
377 self.__applied_file = os.path.join(self._dir(), 'applied')
378 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
379 self.__hidden_file = os.path.join(self._dir(), 'hidden')
381 # where this series keeps its patches
382 self.__patch_dir = os.path.join(self._dir(), 'patches')
385 self.__trash_dir = os.path.join(self._dir(), 'trash')
387 def __patch_name_valid(self, name):
388 """Raise an exception if the patch name is not valid.
390 if not name or re.search('[^\w.-]', name):
391 raise StackException, 'Invalid patch name: "%s"' % name
393 def get_patch(self, name):
394 """Return a Patch object for the given name
396 return Patch(name, self.__patch_dir, self.__refs_base)
398 def get_current_patch(self):
399 """Return a Patch object representing the topmost patch, or
400 None if there is no such patch."""
401 crt = self.get_current()
404 return self.get_patch(crt)
406 def get_current(self):
407 """Return the name of the topmost patch, or None if there is
410 applied = self.get_applied()
411 except StackException:
412 # No "applied" file: branch is not initialized.
417 # No patches applied.
420 def get_applied(self):
421 if not os.path.isfile(self.__applied_file):
422 raise StackException, 'Branch "%s" not initialised' % self.get_name()
423 return read_strings(self.__applied_file)
425 def set_applied(self, applied):
426 write_strings(self.__applied_file, applied)
428 def get_unapplied(self):
429 if not os.path.isfile(self.__unapplied_file):
430 raise StackException, 'Branch "%s" not initialised' % self.get_name()
431 return read_strings(self.__unapplied_file)
433 def set_unapplied(self, unapplied):
434 write_strings(self.__unapplied_file, unapplied)
436 def get_hidden(self):
437 if not os.path.isfile(self.__hidden_file):
439 return read_strings(self.__hidden_file)
442 # Return the parent of the bottommost patch, if there is one.
443 if os.path.isfile(self.__applied_file):
444 bottommost = file(self.__applied_file).readline().strip()
446 return self.get_patch(bottommost).get_bottom()
447 # No bottommost patch, so just return HEAD
448 return git.get_head()
450 def get_parent_remote(self):
451 value = config.get('branch.%s.remote' % self.get_name())
454 elif 'origin' in git.remotes_list():
455 out.note(('No parent remote declared for stack "%s",'
456 ' defaulting to "origin".' % self.get_name()),
457 ('Consider setting "branch.%s.remote" and'
458 ' "branch.%s.merge" with "git config".'
459 % (self.get_name(), self.get_name())))
462 raise StackException, 'Cannot find a parent remote for "%s"' % self.get_name()
464 def __set_parent_remote(self, remote):
465 value = config.set('branch.%s.remote' % self.get_name(), remote)
467 def get_parent_branch(self):
468 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
471 elif git.rev_parse('heads/origin'):
472 out.note(('No parent branch declared for stack "%s",'
473 ' defaulting to "heads/origin".' % self.get_name()),
474 ('Consider setting "branch.%s.stgit.parentbranch"'
475 ' with "git config".' % self.get_name()))
476 return 'heads/origin'
478 raise StackException, 'Cannot find a parent branch for "%s"' % self.get_name()
480 def __set_parent_branch(self, name):
481 if config.get('branch.%s.remote' % self.get_name()):
482 # Never set merge if remote is not set to avoid
483 # possibly-erroneous lookups into 'origin'
484 config.set('branch.%s.merge' % self.get_name(), name)
485 config.set('branch.%s.stgit.parentbranch' % self.get_name(), name)
487 def set_parent(self, remote, localbranch):
490 self.__set_parent_remote(remote)
491 self.__set_parent_branch(localbranch)
492 # We'll enforce this later
494 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.get_name()
496 def __patch_is_current(self, patch):
497 return patch.get_name() == self.get_current()
499 def patch_applied(self, name):
500 """Return true if the patch exists in the applied list
502 return name in self.get_applied()
504 def patch_unapplied(self, name):
505 """Return true if the patch exists in the unapplied list
507 return name in self.get_unapplied()
509 def patch_hidden(self, name):
510 """Return true if the patch is hidden.
512 return name in self.get_hidden()
514 def patch_exists(self, name):
515 """Return true if there is a patch with the given name, false
517 return self.patch_applied(name) or self.patch_unapplied(name) \
518 or self.patch_hidden(name)
520 def init(self, create_at=False, parent_remote=None, parent_branch=None):
521 """Initialises the stgit series
523 if self.is_initialised():
524 raise StackException, '%s already initialized' % self.get_name()
525 for d in [self._dir()]:
526 if os.path.exists(d):
527 raise StackException, '%s already exists' % d
529 if (create_at!=False):
530 git.create_branch(self.get_name(), create_at)
532 os.makedirs(self.__patch_dir)
534 self.set_parent(parent_remote, parent_branch)
536 self.create_empty_field('applied')
537 self.create_empty_field('unapplied')
539 config.set(stackupgrade.format_version_key(self.get_name()),
540 str(stackupgrade.FORMAT_VERSION))
542 def rename(self, to_name):
545 to_stack = Series(to_name)
547 if to_stack.is_initialised():
548 raise StackException, '"%s" already exists' % to_stack.get_name()
550 patches = self.get_applied() + self.get_unapplied()
552 git.rename_branch(self.get_name(), to_name)
554 for patch in patches:
555 git.rename_ref('refs/patches/%s/%s' % (self.get_name(), patch),
556 'refs/patches/%s/%s' % (to_name, patch))
557 git.rename_ref('refs/patches/%s/%s.log' % (self.get_name(), patch),
558 'refs/patches/%s/%s.log' % (to_name, patch))
559 if os.path.isdir(self._dir()):
560 rename(os.path.join(self._basedir(), 'patches'),
561 self.get_name(), to_stack.get_name())
563 # Rename the config section
564 for k in ['branch.%s', 'branch.%s.stgit']:
565 config.rename_section(k % self.get_name(), k % to_name)
567 self.__init__(to_name)
569 def clone(self, target_series):
573 # allow cloning of branches not under StGIT control
574 base = self.get_base()
576 base = git.get_head()
577 Series(target_series).init(create_at = base)
578 new_series = Series(target_series)
580 # generate an artificial description file
581 new_series.set_description('clone of "%s"' % self.get_name())
583 # clone self's entire series as unapplied patches
585 # allow cloning of branches not under StGIT control
586 applied = self.get_applied()
587 unapplied = self.get_unapplied()
588 patches = applied + unapplied
591 patches = applied = unapplied = []
593 patch = self.get_patch(p)
594 newpatch = new_series.new_patch(p, message = patch.get_description(),
595 can_edit = False, unapplied = True,
596 bottom = patch.get_bottom(),
597 top = patch.get_top(),
598 author_name = patch.get_authname(),
599 author_email = patch.get_authemail(),
600 author_date = patch.get_authdate())
602 out.info('Setting log to %s' % patch.get_log())
603 newpatch.set_log(patch.get_log())
605 out.info('No log for %s' % p)
607 # fast forward the cloned series to self's top
608 new_series.forward_patches(applied)
610 # Clone parent informations
611 value = config.get('branch.%s.remote' % self.get_name())
613 config.set('branch.%s.remote' % target_series, value)
615 value = config.get('branch.%s.merge' % self.get_name())
617 config.set('branch.%s.merge' % target_series, value)
619 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
621 config.set('branch.%s.stgit.parentbranch' % target_series, value)
623 def delete(self, force = False):
624 """Deletes an stgit series
626 if self.is_initialised():
627 patches = self.get_unapplied() + self.get_applied()
628 if not force and patches:
629 raise StackException, \
630 'Cannot delete: the series still contains patches'
632 self.get_patch(p).delete()
634 # remove the trash directory if any
635 if os.path.exists(self.__trash_dir):
636 for fname in os.listdir(self.__trash_dir):
637 os.remove(os.path.join(self.__trash_dir, fname))
638 os.rmdir(self.__trash_dir)
640 # FIXME: find a way to get rid of those manual removals
641 # (move functionality to StgitObject ?)
642 if os.path.exists(self.__applied_file):
643 os.remove(self.__applied_file)
644 if os.path.exists(self.__unapplied_file):
645 os.remove(self.__unapplied_file)
646 if os.path.exists(self.__hidden_file):
647 os.remove(self.__hidden_file)
648 if os.path.exists(self._dir()+'/orig-base'):
649 os.remove(self._dir()+'/orig-base')
651 if not os.listdir(self.__patch_dir):
652 os.rmdir(self.__patch_dir)
654 out.warn('Patch directory %s is not empty' % self.__patch_dir)
657 os.removedirs(self._dir())
659 raise StackException('Series directory %s is not empty'
663 git.delete_branch(self.get_name())
665 out.warn('Could not delete branch "%s"' % self.get_name())
667 config.remove_section('branch.%s' % self.get_name())
668 config.remove_section('branch.%s.stgit' % self.get_name())
670 def refresh_patch(self, files = None, message = None, edit = False,
674 author_name = None, author_email = None,
676 committer_name = None, committer_email = None,
677 backup = True, sign_str = None, log = 'refresh',
678 notes = None, bottom = None):
679 """Generates a new commit for the topmost patch
681 patch = self.get_current_patch()
683 raise StackException, 'No patches applied'
685 descr = patch.get_description()
686 if not (message or descr):
692 # TODO: move this out of the stgit.stack module, it is really
693 # for higher level commands to handle the user interaction
694 if not message and edit:
695 descr = edit_file(self, descr.rstrip(), \
696 'Please edit the description for patch "%s" ' \
697 'above.' % patch.get_name(), show_patch)
700 author_name = patch.get_authname()
702 author_email = patch.get_authemail()
704 author_date = patch.get_authdate()
705 if not committer_name:
706 committer_name = patch.get_commname()
707 if not committer_email:
708 committer_email = patch.get_commemail()
710 descr = add_sign_line(descr, sign_str, committer_name, committer_email)
713 bottom = patch.get_bottom()
716 tree_id = git.get_commit(bottom).get_tree()
720 commit_id = git.commit(files = files,
721 message = descr, parents = [bottom],
722 cache_update = cache_update,
726 author_name = author_name,
727 author_email = author_email,
728 author_date = author_date,
729 committer_name = committer_name,
730 committer_email = committer_email)
732 patch.set_top(commit_id, backup = backup)
733 patch.set_description(descr)
734 patch.set_authname(author_name)
735 patch.set_authemail(author_email)
736 patch.set_authdate(author_date)
737 patch.set_commname(committer_name)
738 patch.set_commemail(committer_email)
741 self.log_patch(patch, log, notes)
745 def undo_refresh(self):
746 """Undo the patch boundaries changes caused by 'refresh'
748 name = self.get_current()
751 patch = self.get_patch(name)
752 old_bottom = patch.get_old_bottom()
753 old_top = patch.get_old_top()
755 # the bottom of the patch is not changed by refresh. If the
756 # old_bottom is different, there wasn't any previous 'refresh'
757 # command (probably only a 'push')
758 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
759 raise StackException, 'No undo information available'
761 git.reset(tree_id = old_top, check_out = False)
762 if patch.restore_old_boundaries():
763 self.log_patch(patch, 'undo')
765 def new_patch(self, name, message = None, can_edit = True,
766 unapplied = False, show_patch = False,
767 top = None, bottom = None, commit = True,
768 author_name = None, author_email = None, author_date = None,
769 committer_name = None, committer_email = None,
770 before_existing = False, sign_str = None):
771 """Creates a new patch, either pointing to an existing commit object,
772 or by creating a new commit object.
775 assert commit or (top and bottom)
776 assert not before_existing or (top and bottom)
777 assert not (commit and before_existing)
778 assert (top and bottom) or (not top and not bottom)
779 assert commit or (not top or (bottom == git.get_commit(top).get_parent()))
782 self.__patch_name_valid(name)
783 if self.patch_exists(name):
784 raise StackException, 'Patch "%s" already exists' % name
786 # TODO: move this out of the stgit.stack module, it is really
787 # for higher level commands to handle the user interaction
789 return add_sign_line(msg, sign_str,
790 committer_name or git.committer().name,
791 committer_email or git.committer().email)
792 if not message and can_edit:
795 'Please enter the description for the patch above.',
798 descr = sign(message)
800 head = git.get_head()
803 name = make_patch_name(descr, self.patch_exists)
805 patch = self.get_patch(name)
808 patch.set_description(descr)
809 patch.set_authname(author_name)
810 patch.set_authemail(author_email)
811 patch.set_authdate(author_date)
812 patch.set_commname(committer_name)
813 patch.set_commemail(committer_email)
816 insert_string(self.__applied_file, patch.get_name())
818 patches = [patch.get_name()] + self.get_unapplied()
819 write_strings(self.__unapplied_file, patches)
822 append_string(self.__applied_file, patch.get_name())
827 top_commit = git.get_commit(top)
830 top_commit = git.get_commit(head)
832 # create a commit for the patch (may be empty if top == bottom);
833 # only commit on top of the current branch
834 assert(unapplied or bottom == head)
835 commit_id = git.commit(message = descr, parents = [bottom],
836 cache_update = False,
837 tree_id = top_commit.get_tree(),
838 allowempty = True, set_head = set_head,
839 author_name = author_name,
840 author_email = author_email,
841 author_date = author_date,
842 committer_name = committer_name,
843 committer_email = committer_email)
844 # set the patch top to the new commit
845 patch.set_top(commit_id)
849 self.log_patch(patch, 'new')
853 def delete_patch(self, name, keep_log = False):
856 self.__patch_name_valid(name)
857 patch = self.get_patch(name)
859 if self.__patch_is_current(patch):
861 elif self.patch_applied(name):
862 raise StackException, 'Cannot remove an applied patch, "%s", ' \
863 'which is not current' % name
864 elif not name in self.get_unapplied():
865 raise StackException, 'Unknown patch "%s"' % name
867 # save the commit id to a trash file
868 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
870 patch.delete(keep_log = keep_log)
872 unapplied = self.get_unapplied()
873 unapplied.remove(name)
874 write_strings(self.__unapplied_file, unapplied)
876 def forward_patches(self, names):
877 """Try to fast-forward an array of patches.
879 On return, patches in names[0:returned_value] have been pushed on the
880 stack. Apply the rest with push_patch
882 unapplied = self.get_unapplied()
888 assert(name in unapplied)
890 patch = self.get_patch(name)
893 bottom = patch.get_bottom()
894 top = patch.get_top()
896 # top != bottom always since we have a commit for each patch
898 # reset the backup information. No logging since the
899 # patch hasn't changed
900 patch.set_top(top, backup = True)
903 head_tree = git.get_commit(head).get_tree()
904 bottom_tree = git.get_commit(bottom).get_tree()
905 if head_tree == bottom_tree:
906 # We must just reparent this patch and create a new commit
908 descr = patch.get_description()
909 author_name = patch.get_authname()
910 author_email = patch.get_authemail()
911 author_date = patch.get_authdate()
912 committer_name = patch.get_commname()
913 committer_email = patch.get_commemail()
915 top_tree = git.get_commit(top).get_tree()
917 top = git.commit(message = descr, parents = [head],
918 cache_update = False,
921 author_name = author_name,
922 author_email = author_email,
923 author_date = author_date,
924 committer_name = committer_name,
925 committer_email = committer_email)
927 patch.set_top(top, backup = True)
929 self.log_patch(patch, 'push(f)')
932 # stop the fast-forwarding, must do a real merge
936 unapplied.remove(name)
943 append_strings(self.__applied_file, names[0:forwarded])
944 write_strings(self.__unapplied_file, unapplied)
948 def merged_patches(self, names):
949 """Test which patches were merged upstream by reverse-applying
950 them in reverse order. The function returns the list of
951 patches detected to have been applied. The state of the tree
952 is restored to the original one
954 patches = [self.get_patch(name) for name in names]
959 if git.apply_diff(p.get_top(), p.get_bottom()):
960 merged.append(p.get_name())
967 def push_empty_patch(self, name):
968 """Pushes an empty patch on the stack
970 unapplied = self.get_unapplied()
971 assert(name in unapplied)
973 # patch = self.get_patch(name)
974 head = git.get_head()
976 append_string(self.__applied_file, name)
978 unapplied.remove(name)
979 write_strings(self.__unapplied_file, unapplied)
981 self.refresh_patch(bottom = head, cache_update = False, log = 'push(m)')
983 def push_patch(self, name):
984 """Pushes a patch on the stack
986 unapplied = self.get_unapplied()
987 assert(name in unapplied)
989 patch = self.get_patch(name)
991 head = git.get_head()
992 bottom = patch.get_bottom()
993 top = patch.get_top()
994 # top != bottom always since we have a commit for each patch
997 # A fast-forward push. Just reset the backup
998 # information. No need for logging
999 patch.set_top(top, backup = True)
1002 append_string(self.__applied_file, name)
1004 unapplied.remove(name)
1005 write_strings(self.__unapplied_file, unapplied)
1008 # Need to create a new commit an merge in the old patch
1012 # Try the fast applying first. If this fails, fall back to the
1014 if not git.apply_diff(bottom, top):
1015 # if git.apply_diff() fails, the patch requires a diff3
1016 # merge and can be reported as modified
1019 # merge can fail but the patch needs to be pushed
1021 git.merge_recursive(bottom, head, top)
1022 except git.GitConflictException, ex:
1024 except git.GitException, ex:
1025 out.error('The merge failed during "push".',
1026 'Revert the operation with "push --undo".')
1028 append_string(self.__applied_file, name)
1030 unapplied.remove(name)
1031 write_strings(self.__unapplied_file, unapplied)
1034 # if the merge was OK and no conflicts, just refresh the patch
1035 # The GIT cache was already updated by the merge operation
1040 self.refresh_patch(bottom = head, cache_update = False, log = log)
1042 # we make the patch empty, with the merged state in the
1044 self.refresh_patch(bottom = head, cache_update = False,
1045 empty = True, log = 'push(c)')
1046 raise StackException, str(ex)
1050 def undo_push(self):
1051 name = self.get_current()
1054 patch = self.get_patch(name)
1055 old_bottom = patch.get_old_bottom()
1056 old_top = patch.get_old_top()
1058 # the top of the patch is changed by a push operation only
1059 # together with the bottom (otherwise the top was probably
1060 # modified by 'refresh'). If they are both unchanged, there
1061 # was a fast forward
1062 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1063 raise StackException, 'No undo information available'
1066 self.pop_patch(name)
1067 ret = patch.restore_old_boundaries()
1069 self.log_patch(patch, 'undo')
1073 def pop_patch(self, name, keep = False):
1074 """Pops the top patch from the stack
1076 applied = self.get_applied()
1078 assert(name in applied)
1080 patch = self.get_patch(name)
1082 if git.get_head_file() == self.get_name():
1083 if keep and not git.apply_diff(git.get_head(), patch.get_bottom(),
1084 check_index = False):
1085 raise StackException(
1086 'Failed to pop patches while preserving the local changes')
1087 git.switch(patch.get_bottom(), keep)
1089 git.set_branch(self.get_name(), patch.get_bottom())
1091 # save the new applied list
1092 idx = applied.index(name) + 1
1094 popped = applied[:idx]
1096 unapplied = popped + self.get_unapplied()
1097 write_strings(self.__unapplied_file, unapplied)
1101 write_strings(self.__applied_file, applied)
1103 def empty_patch(self, name):
1104 """Returns True if the patch is empty
1106 self.__patch_name_valid(name)
1107 patch = self.get_patch(name)
1108 bottom = patch.get_bottom()
1109 top = patch.get_top()
1113 elif git.get_commit(top).get_tree() \
1114 == git.get_commit(bottom).get_tree():
1119 def rename_patch(self, oldname, newname):
1120 self.__patch_name_valid(newname)
1122 applied = self.get_applied()
1123 unapplied = self.get_unapplied()
1125 if oldname == newname:
1126 raise StackException, '"To" name and "from" name are the same'
1128 if newname in applied or newname in unapplied:
1129 raise StackException, 'Patch "%s" already exists' % newname
1131 if oldname in unapplied:
1132 self.get_patch(oldname).rename(newname)
1133 unapplied[unapplied.index(oldname)] = newname
1134 write_strings(self.__unapplied_file, unapplied)
1135 elif oldname in applied:
1136 self.get_patch(oldname).rename(newname)
1138 applied[applied.index(oldname)] = newname
1139 write_strings(self.__applied_file, applied)
1141 raise StackException, 'Unknown patch "%s"' % oldname
1143 def log_patch(self, patch, message, notes = None):
1144 """Generate a log commit for a patch
1146 top = git.get_commit(patch.get_top())
1147 old_log = patch.get_log()
1150 # replace the current log entry
1152 raise StackException, \
1153 'No log entry to annotate for patch "%s"' \
1156 log_commit = git.get_commit(old_log)
1157 msg = log_commit.get_log().split('\n')[0]
1158 log_parent = log_commit.get_parent()
1160 parents = [log_parent]
1164 # generate a new log entry
1166 msg = '%s\t%s' % (message, top.get_id_hash())
1173 msg += '\n\n' + notes
1175 log = git.commit(message = msg, parents = parents,
1176 cache_update = False, tree_id = top.get_tree(),
1180 def hide_patch(self, name):
1181 """Add the patch to the hidden list.
1183 unapplied = self.get_unapplied()
1184 if name not in unapplied:
1185 # keep the checking order for backward compatibility with
1186 # the old hidden patches functionality
1187 if self.patch_applied(name):
1188 raise StackException, 'Cannot hide applied patch "%s"' % name
1189 elif self.patch_hidden(name):
1190 raise StackException, 'Patch "%s" already hidden' % name
1192 raise StackException, 'Unknown patch "%s"' % name
1194 if not self.patch_hidden(name):
1195 # check needed for backward compatibility with the old
1196 # hidden patches functionality
1197 append_string(self.__hidden_file, name)
1199 unapplied.remove(name)
1200 write_strings(self.__unapplied_file, unapplied)
1202 def unhide_patch(self, name):
1203 """Remove the patch from the hidden list.
1205 hidden = self.get_hidden()
1206 if not name in hidden:
1207 if self.patch_applied(name) or self.patch_unapplied(name):
1208 raise StackException, 'Patch "%s" not hidden' % name
1210 raise StackException, 'Unknown patch "%s"' % name
1213 write_strings(self.__hidden_file, hidden)
1215 if not self.patch_applied(name) and not self.patch_unapplied(name):
1216 # check needed for backward compatibility with the old
1217 # hidden patches functionality
1218 append_string(self.__unapplied_file, name)