Correctly handle refs/patches on series rename
[stgit/ydirson.git] / stgit / stack.py
blob975ac210563e159492ca99ca030b1d39976ab314
1 """Basic quilt-like functionality
2 """
4 __copyright__ = """
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
19 """
21 import sys, os
23 from stgit.utils import *
24 from stgit import git, basedir
25 from stgit.config import config
28 # stack exception class
29 class StackException(Exception):
30 pass
32 class FilterUntil:
33 def __init__(self):
34 self.should_print = True
35 def __call__(self, x, until_test, prefix):
36 if until_test(x):
37 self.should_print = False
38 if self.should_print:
39 return x[0:len(prefix)] != prefix
40 return False
43 # Functions
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
48 def __clean_comments(f):
49 """Removes lines marked for status in a commit file
50 """
51 f.seek(0)
53 # remove status-prefixed lines
54 lines = f.readlines()
56 patch_filter = FilterUntil()
57 until_test = lambda t: t == (__patch_prefix + '\n')
58 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
60 # remove empty lines at the end
61 while len(lines) != 0 and lines[-1] == '\n':
62 del lines[-1]
64 f.seek(0); f.truncate()
65 f.writelines(lines)
67 def edit_file(series, line, comment, show_patch = True):
68 fname = '.stgit.msg'
69 tmpl = os.path.join(basedir.get(), 'patchdescr.tmpl')
71 f = file(fname, 'w+')
72 if line:
73 print >> f, line
74 elif os.path.isfile(tmpl):
75 print >> f, file(tmpl).read().rstrip()
76 else:
77 print >> f
78 print >> f, __comment_prefix, comment
79 print >> f, __comment_prefix, \
80 'Lines prefixed with "%s" will be automatically removed.' \
81 % __comment_prefix
82 print >> f, __comment_prefix, \
83 'Trailing empty lines will be automatically removed.'
85 if show_patch:
86 print >> f, __patch_prefix
87 # series.get_patch(series.get_current()).get_top()
88 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
90 #Vim modeline must be near the end.
91 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
92 f.close()
94 # the editor
95 if config.has_option('stgit', 'editor'):
96 editor = config.get('stgit', 'editor')
97 elif 'EDITOR' in os.environ:
98 editor = os.environ['EDITOR']
99 else:
100 editor = 'vi'
101 editor += ' %s' % fname
103 print 'Invoking the editor: "%s"...' % editor,
104 sys.stdout.flush()
105 print 'done (exit code: %d)' % os.system(editor)
107 f = file(fname, 'r+')
109 __clean_comments(f)
110 f.seek(0)
111 result = f.read()
113 f.close()
114 os.remove(fname)
116 return result
119 # Classes
122 class Patch:
123 """Basic patch implementation
125 def __init__(self, name, series_dir, refs_dir):
126 self.__series_dir = series_dir
127 self.__name = name
128 self.__dir = os.path.join(self.__series_dir, self.__name)
129 self.__refs_dir = refs_dir
130 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
132 def create(self):
133 os.mkdir(self.__dir)
134 create_empty_file(os.path.join(self.__dir, 'bottom'))
135 create_empty_file(os.path.join(self.__dir, 'top'))
137 def delete(self):
138 for f in os.listdir(self.__dir):
139 os.remove(os.path.join(self.__dir, f))
140 os.rmdir(self.__dir)
141 os.remove(self.__top_ref_file)
143 def get_name(self):
144 return self.__name
146 def rename(self, newname):
147 olddir = self.__dir
148 old_ref_file = self.__top_ref_file
149 self.__name = newname
150 self.__dir = os.path.join(self.__series_dir, self.__name)
151 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
153 os.rename(olddir, self.__dir)
154 os.rename(old_ref_file, self.__top_ref_file)
156 def __update_top_ref(self, ref):
157 write_string(self.__top_ref_file, ref)
159 def update_top_ref(self):
160 top = self.get_top()
161 if top:
162 self.__update_top_ref(top)
164 def __get_field(self, name, multiline = False):
165 id_file = os.path.join(self.__dir, name)
166 if os.path.isfile(id_file):
167 line = read_string(id_file, multiline)
168 if line == '':
169 return None
170 else:
171 return line
172 else:
173 return None
175 def __set_field(self, name, value, multiline = False):
176 fname = os.path.join(self.__dir, name)
177 if value and value != '':
178 write_string(fname, value, multiline)
179 elif os.path.isfile(fname):
180 os.remove(fname)
182 def get_old_bottom(self):
183 return self.__get_field('bottom.old')
185 def get_bottom(self):
186 return self.__get_field('bottom')
188 def set_bottom(self, value, backup = False):
189 if backup:
190 curr = self.__get_field('bottom')
191 self.__set_field('bottom.old', curr)
192 self.__set_field('bottom', value)
194 def get_old_top(self):
195 return self.__get_field('top.old')
197 def get_top(self):
198 return self.__get_field('top')
200 def set_top(self, value, backup = False):
201 if backup:
202 curr = self.__get_field('top')
203 self.__set_field('top.old', curr)
204 self.__set_field('top', value)
205 self.__update_top_ref(value)
207 def restore_old_boundaries(self):
208 bottom = self.__get_field('bottom.old')
209 top = self.__get_field('top.old')
211 if top and bottom:
212 self.__set_field('bottom', bottom)
213 self.__set_field('top', top)
214 self.__update_top_ref(top)
215 return True
216 else:
217 return False
219 def get_description(self):
220 return self.__get_field('description', True)
222 def set_description(self, line):
223 self.__set_field('description', line, True)
225 def get_authname(self):
226 return self.__get_field('authname')
228 def set_authname(self, name):
229 if not name:
230 if config.has_option('stgit', 'authname'):
231 name = config.get('stgit', 'authname')
232 elif 'GIT_AUTHOR_NAME' in os.environ:
233 name = os.environ['GIT_AUTHOR_NAME']
234 self.__set_field('authname', name)
236 def get_authemail(self):
237 return self.__get_field('authemail')
239 def set_authemail(self, address):
240 if not address:
241 if config.has_option('stgit', 'authemail'):
242 address = config.get('stgit', 'authemail')
243 elif 'GIT_AUTHOR_EMAIL' in os.environ:
244 address = os.environ['GIT_AUTHOR_EMAIL']
245 self.__set_field('authemail', address)
247 def get_authdate(self):
248 return self.__get_field('authdate')
250 def set_authdate(self, date):
251 if not date and 'GIT_AUTHOR_DATE' in os.environ:
252 date = os.environ['GIT_AUTHOR_DATE']
253 self.__set_field('authdate', date)
255 def get_commname(self):
256 return self.__get_field('commname')
258 def set_commname(self, name):
259 if not name:
260 if config.has_option('stgit', 'commname'):
261 name = config.get('stgit', 'commname')
262 elif 'GIT_COMMITTER_NAME' in os.environ:
263 name = os.environ['GIT_COMMITTER_NAME']
264 self.__set_field('commname', name)
266 def get_commemail(self):
267 return self.__get_field('commemail')
269 def set_commemail(self, address):
270 if not address:
271 if config.has_option('stgit', 'commemail'):
272 address = config.get('stgit', 'commemail')
273 elif 'GIT_COMMITTER_EMAIL' in os.environ:
274 address = os.environ['GIT_COMMITTER_EMAIL']
275 self.__set_field('commemail', address)
278 class Series:
279 """Class including the operations on series
281 def __init__(self, name = None):
282 """Takes a series name as the parameter.
284 try:
285 if name:
286 self.__name = name
287 else:
288 self.__name = git.get_head_file()
289 self.__base_dir = basedir.get()
290 except git.GitException, ex:
291 raise StackException, 'GIT tree not initialised: %s' % ex
293 self.__series_dir = os.path.join(self.__base_dir, 'patches',
294 self.__name)
295 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
296 self.__name)
297 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
298 self.__name)
300 self.__applied_file = os.path.join(self.__series_dir, 'applied')
301 self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied')
302 self.__current_file = os.path.join(self.__series_dir, 'current')
303 self.__descr_file = os.path.join(self.__series_dir, 'description')
305 # where this series keeps its patches
306 self.__patch_dir = os.path.join(self.__series_dir, 'patches')
307 if not os.path.isdir(self.__patch_dir):
308 self.__patch_dir = self.__series_dir
310 # if no __refs_dir, create and populate it (upgrade old repositories)
311 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
312 os.makedirs(self.__refs_dir)
313 for patch in self.get_applied() + self.get_unapplied():
314 self.get_patch(patch).update_top_ref()
316 def get_branch(self):
317 """Return the branch name for the Series object
319 return self.__name
321 def __set_current(self, name):
322 """Sets the topmost patch
324 if name:
325 write_string(self.__current_file, name)
326 else:
327 create_empty_file(self.__current_file)
329 def get_patch(self, name):
330 """Return a Patch object for the given name
332 return Patch(name, self.__patch_dir, self.__refs_dir)
334 def get_current(self):
335 """Return a Patch object representing the topmost patch
337 if os.path.isfile(self.__current_file):
338 name = read_string(self.__current_file)
339 else:
340 return None
341 if name == '':
342 return None
343 else:
344 return name
346 def get_applied(self):
347 if not os.path.isfile(self.__applied_file):
348 raise StackException, 'Branch "%s" not initialised' % self.__name
349 f = file(self.__applied_file)
350 names = [line.strip() for line in f.readlines()]
351 f.close()
352 return names
354 def get_unapplied(self):
355 if not os.path.isfile(self.__unapplied_file):
356 raise StackException, 'Branch "%s" not initialised' % self.__name
357 f = file(self.__unapplied_file)
358 names = [line.strip() for line in f.readlines()]
359 f.close()
360 return names
362 def get_base_file(self):
363 self.__begin_stack_check()
364 return self.__base_file
366 def get_protected(self):
367 return os.path.isfile(os.path.join(self.__series_dir, 'protected'))
369 def protect(self):
370 protect_file = os.path.join(self.__series_dir, 'protected')
371 if not os.path.isfile(protect_file):
372 create_empty_file(protect_file)
374 def unprotect(self):
375 protect_file = os.path.join(self.__series_dir, 'protected')
376 if os.path.isfile(protect_file):
377 os.remove(protect_file)
379 def get_description(self):
380 if os.path.isfile(self.__descr_file):
381 return read_string(self.__descr_file)
382 else:
383 return ''
385 def __patch_is_current(self, patch):
386 return patch.get_name() == read_string(self.__current_file)
388 def __patch_applied(self, name):
389 """Return true if the patch exists in the applied list
391 return name in self.get_applied()
393 def __patch_unapplied(self, name):
394 """Return true if the patch exists in the unapplied list
396 return name in self.get_unapplied()
398 def __begin_stack_check(self):
399 """Save the current HEAD into .git/refs/heads/base if the stack
400 is empty
402 if len(self.get_applied()) == 0:
403 head = git.get_head()
404 write_string(self.__base_file, head)
406 def __end_stack_check(self):
407 """Remove .git/refs/heads/base if the stack is empty.
408 This warning should never happen
410 if len(self.get_applied()) == 0 \
411 and read_string(self.__base_file) != git.get_head():
412 print 'Warning: stack empty but the HEAD and base are different'
414 def head_top_equal(self):
415 """Return true if the head and the top are the same
417 crt = self.get_current()
418 if not crt:
419 # we don't care, no patches applied
420 return True
421 return git.get_head() == Patch(crt, self.__patch_dir,
422 self.__refs_dir).get_top()
424 def is_initialised(self):
425 """Checks if series is already initialised
427 return os.path.isdir(self.__patch_dir)
429 def init(self, create_at=False):
430 """Initialises the stgit series
432 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
434 if os.path.exists(self.__patch_dir):
435 raise StackException, self.__patch_dir + ' already exists'
436 if os.path.exists(self.__refs_dir):
437 raise StackException, self.__refs_dir + ' already exists'
438 if os.path.exists(self.__base_file):
439 raise StackException, self.__base_file + ' already exists'
441 if (create_at!=False):
442 git.create_branch(self.__name, create_at)
444 os.makedirs(self.__patch_dir)
446 if not os.path.isdir(bases_dir):
447 os.makedirs(bases_dir)
449 create_empty_file(self.__applied_file)
450 create_empty_file(self.__unapplied_file)
451 create_empty_file(self.__descr_file)
452 os.makedirs(os.path.join(self.__series_dir, 'patches'))
453 os.makedirs(self.__refs_dir)
454 self.__begin_stack_check()
456 def convert(self):
457 """Either convert to use a separate patch directory, or
458 unconvert to place the patches in the same directory with
459 series control files
461 if self.__patch_dir == self.__series_dir:
462 print 'Converting old-style to new-style...',
463 sys.stdout.flush()
465 self.__patch_dir = os.path.join(self.__series_dir, 'patches')
466 os.makedirs(self.__patch_dir)
468 for p in self.get_applied() + self.get_unapplied():
469 src = os.path.join(self.__series_dir, p)
470 dest = os.path.join(self.__patch_dir, p)
471 os.rename(src, dest)
473 print 'done'
475 else:
476 print 'Converting new-style to old-style...',
477 sys.stdout.flush()
479 for p in self.get_applied() + self.get_unapplied():
480 src = os.path.join(self.__patch_dir, p)
481 dest = os.path.join(self.__series_dir, p)
482 os.rename(src, dest)
484 if not os.listdir(self.__patch_dir):
485 os.rmdir(self.__patch_dir)
486 print 'done'
487 else:
488 print 'Patch directory %s is not empty.' % self.__name
490 self.__patch_dir = self.__series_dir
492 def rename(self, to_name):
493 """Renames a series
495 to_stack = Series(to_name)
497 if to_stack.is_initialised():
498 raise StackException, '"%s" already exists' % to_stack.get_branch()
499 if os.path.exists(to_stack.__base_file):
500 os.remove(to_stack.__base_file)
502 git.rename_branch(self.__name, to_name)
504 if os.path.isdir(self.__series_dir):
505 os.rename(self.__series_dir, to_stack.__series_dir)
506 if os.path.exists(self.__base_file):
507 os.rename(self.__base_file, to_stack.__base_file)
508 if os.path.exists(self.__refs_dir):
509 os.rename(self.__refs_dir, to_stack.__refs_dir)
511 self.__init__(to_name)
513 def clone(self, target_series):
514 """Clones a series
516 base = read_string(self.get_base_file())
517 Series(target_series).init(create_at = base)
518 new_series = Series(target_series)
520 # generate an artificial description file
521 write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
523 # clone self's entire series as unapplied patches
524 patches = self.get_applied() + self.get_unapplied()
525 patches.reverse()
526 for p in patches:
527 patch = self.get_patch(p)
528 new_series.new_patch(p, message = patch.get_description(),
529 can_edit = False, unapplied = True,
530 bottom = patch.get_bottom(),
531 top = patch.get_top(),
532 author_name = patch.get_authname(),
533 author_email = patch.get_authemail(),
534 author_date = patch.get_authdate())
536 # fast forward the cloned series to self's top
537 new_series.forward_patches(self.get_applied())
539 def delete(self, force = False):
540 """Deletes an stgit series
542 if self.is_initialised():
543 patches = self.get_unapplied() + self.get_applied()
544 if not force and patches:
545 raise StackException, \
546 'Cannot delete: the series still contains patches'
547 for p in patches:
548 Patch(p, self.__patch_dir, self.__refs_dir).delete()
550 if os.path.exists(self.__applied_file):
551 os.remove(self.__applied_file)
552 if os.path.exists(self.__unapplied_file):
553 os.remove(self.__unapplied_file)
554 if os.path.exists(self.__current_file):
555 os.remove(self.__current_file)
556 if os.path.exists(self.__descr_file):
557 os.remove(self.__descr_file)
558 if not os.listdir(self.__patch_dir):
559 os.rmdir(self.__patch_dir)
560 else:
561 print 'Patch directory %s is not empty.' % self.__name
562 if not os.listdir(self.__series_dir):
563 os.rmdir(self.__series_dir)
564 else:
565 print 'Series directory %s is not empty.' % self.__name
566 if not os.listdir(self.__refs_dir):
567 os.rmdir(self.__refs_dir)
568 else:
569 print 'Refs directory %s is not empty.' % self.__refs_dir
571 if os.path.exists(self.__base_file):
572 os.remove(self.__base_file)
574 def refresh_patch(self, files = None, message = None, edit = False,
575 show_patch = False,
576 cache_update = True,
577 author_name = None, author_email = None,
578 author_date = None,
579 committer_name = None, committer_email = None,
580 backup = False):
581 """Generates a new commit for the given patch
583 name = self.get_current()
584 if not name:
585 raise StackException, 'No patches applied'
587 patch = Patch(name, self.__patch_dir, self.__refs_dir)
589 descr = patch.get_description()
590 if not (message or descr):
591 edit = True
592 descr = ''
593 elif message:
594 descr = message
596 if not message and edit:
597 descr = edit_file(self, descr.rstrip(), \
598 'Please edit the description for patch "%s" ' \
599 'above.' % name, show_patch)
601 if not author_name:
602 author_name = patch.get_authname()
603 if not author_email:
604 author_email = patch.get_authemail()
605 if not author_date:
606 author_date = patch.get_authdate()
607 if not committer_name:
608 committer_name = patch.get_commname()
609 if not committer_email:
610 committer_email = patch.get_commemail()
612 bottom = patch.get_bottom()
614 commit_id = git.commit(files = files,
615 message = descr, parents = [bottom],
616 cache_update = cache_update,
617 allowempty = True,
618 author_name = author_name,
619 author_email = author_email,
620 author_date = author_date,
621 committer_name = committer_name,
622 committer_email = committer_email)
624 patch.set_bottom(bottom, backup = backup)
625 patch.set_top(commit_id, backup = backup)
626 patch.set_description(descr)
627 patch.set_authname(author_name)
628 patch.set_authemail(author_email)
629 patch.set_authdate(author_date)
630 patch.set_commname(committer_name)
631 patch.set_commemail(committer_email)
633 return commit_id
635 def undo_refresh(self):
636 """Undo the patch boundaries changes caused by 'refresh'
638 name = self.get_current()
639 assert(name)
641 patch = Patch(name, self.__patch_dir, self.__refs_dir)
642 old_bottom = patch.get_old_bottom()
643 old_top = patch.get_old_top()
645 # the bottom of the patch is not changed by refresh. If the
646 # old_bottom is different, there wasn't any previous 'refresh'
647 # command (probably only a 'push')
648 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
649 raise StackException, 'No refresh undo information available'
651 git.reset(tree_id = old_top, check_out = False)
652 patch.restore_old_boundaries()
654 def new_patch(self, name, message = None, can_edit = True,
655 unapplied = False, show_patch = False,
656 top = None, bottom = None,
657 author_name = None, author_email = None, author_date = None,
658 committer_name = None, committer_email = None,
659 before_existing = False):
660 """Creates a new patch
662 if self.__patch_applied(name) or self.__patch_unapplied(name):
663 raise StackException, 'Patch "%s" already exists' % name
665 if not message and can_edit:
666 descr = edit_file(self, None, \
667 'Please enter the description for patch "%s" ' \
668 'above.' % name, show_patch)
669 else:
670 descr = message
672 head = git.get_head()
674 self.__begin_stack_check()
676 patch = Patch(name, self.__patch_dir, self.__refs_dir)
677 patch.create()
679 if bottom:
680 patch.set_bottom(bottom)
681 else:
682 patch.set_bottom(head)
683 if top:
684 patch.set_top(top)
685 else:
686 patch.set_top(head)
688 patch.set_description(descr)
689 patch.set_authname(author_name)
690 patch.set_authemail(author_email)
691 patch.set_authdate(author_date)
692 patch.set_commname(committer_name)
693 patch.set_commemail(committer_email)
695 if unapplied:
696 patches = [patch.get_name()] + self.get_unapplied()
698 f = file(self.__unapplied_file, 'w+')
699 f.writelines([line + '\n' for line in patches])
700 f.close()
701 else:
702 if before_existing:
703 insert_string(self.__applied_file, patch.get_name())
704 if not self.get_current():
705 self.__set_current(name)
706 else:
707 append_string(self.__applied_file, patch.get_name())
708 self.__set_current(name)
710 def delete_patch(self, name):
711 """Deletes a patch
713 patch = Patch(name, self.__patch_dir, self.__refs_dir)
715 if self.__patch_is_current(patch):
716 self.pop_patch(name)
717 elif self.__patch_applied(name):
718 raise StackException, 'Cannot remove an applied patch, "%s", ' \
719 'which is not current' % name
720 elif not name in self.get_unapplied():
721 raise StackException, 'Unknown patch "%s"' % name
723 patch.delete()
725 unapplied = self.get_unapplied()
726 unapplied.remove(name)
727 f = file(self.__unapplied_file, 'w+')
728 f.writelines([line + '\n' for line in unapplied])
729 f.close()
730 self.__begin_stack_check()
732 def forward_patches(self, names):
733 """Try to fast-forward an array of patches.
735 On return, patches in names[0:returned_value] have been pushed on the
736 stack. Apply the rest with push_patch
738 unapplied = self.get_unapplied()
739 self.__begin_stack_check()
741 forwarded = 0
742 top = git.get_head()
744 for name in names:
745 assert(name in unapplied)
747 patch = Patch(name, self.__patch_dir, self.__refs_dir)
749 head = top
750 bottom = patch.get_bottom()
751 top = patch.get_top()
753 # top != bottom always since we have a commit for each patch
754 if head == bottom:
755 # reset the backup information
756 patch.set_bottom(head, backup = True)
757 patch.set_top(top, backup = True)
759 else:
760 head_tree = git.get_commit(head).get_tree()
761 bottom_tree = git.get_commit(bottom).get_tree()
762 if head_tree == bottom_tree:
763 # We must just reparent this patch and create a new commit
764 # for it
765 descr = patch.get_description()
766 author_name = patch.get_authname()
767 author_email = patch.get_authemail()
768 author_date = patch.get_authdate()
769 committer_name = patch.get_commname()
770 committer_email = patch.get_commemail()
772 top_tree = git.get_commit(top).get_tree()
774 top = git.commit(message = descr, parents = [head],
775 cache_update = False,
776 tree_id = top_tree,
777 allowempty = True,
778 author_name = author_name,
779 author_email = author_email,
780 author_date = author_date,
781 committer_name = committer_name,
782 committer_email = committer_email)
784 patch.set_bottom(head, backup = True)
785 patch.set_top(top, backup = True)
786 else:
787 top = head
788 # stop the fast-forwarding, must do a real merge
789 break
791 forwarded+=1
792 unapplied.remove(name)
794 if forwarded == 0:
795 return 0
797 git.switch(top)
799 append_strings(self.__applied_file, names[0:forwarded])
801 f = file(self.__unapplied_file, 'w+')
802 f.writelines([line + '\n' for line in unapplied])
803 f.close()
805 self.__set_current(name)
807 return forwarded
809 def merged_patches(self, names):
810 """Test which patches were merged upstream by reverse-applying
811 them in reverse order. The function returns the list of
812 patches detected to have been applied. The state of the tree
813 is restored to the original one
815 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
816 for name in names]
817 patches.reverse()
819 merged = []
820 for p in patches:
821 if git.apply_diff(p.get_top(), p.get_bottom(), False):
822 merged.append(p.get_name())
823 merged.reverse()
825 git.reset()
827 return merged
829 def push_patch(self, name, empty = False):
830 """Pushes a patch on the stack
832 unapplied = self.get_unapplied()
833 assert(name in unapplied)
835 self.__begin_stack_check()
837 patch = Patch(name, self.__patch_dir, self.__refs_dir)
839 head = git.get_head()
840 bottom = patch.get_bottom()
841 top = patch.get_top()
843 ex = None
844 modified = False
846 # top != bottom always since we have a commit for each patch
847 if empty:
848 # just make an empty patch (top = bottom = HEAD). This
849 # option is useful to allow undoing already merged
850 # patches. The top is updated by refresh_patch since we
851 # need an empty commit
852 patch.set_bottom(head, backup = True)
853 patch.set_top(head, backup = True)
854 modified = True
855 elif head == bottom:
856 # reset the backup information
857 patch.set_bottom(bottom, backup = True)
858 patch.set_top(top, backup = True)
860 git.switch(top)
861 else:
862 # new patch needs to be refreshed.
863 # The current patch is empty after merge.
864 patch.set_bottom(head, backup = True)
865 patch.set_top(head, backup = True)
867 # Try the fast applying first. If this fails, fall back to the
868 # three-way merge
869 if not git.apply_diff(bottom, top):
870 # if git.apply_diff() fails, the patch requires a diff3
871 # merge and can be reported as modified
872 modified = True
874 # merge can fail but the patch needs to be pushed
875 try:
876 git.merge(bottom, head, top)
877 except git.GitException, ex:
878 print >> sys.stderr, \
879 'The merge failed during "push". ' \
880 'Use "refresh" after fixing the conflicts'
882 append_string(self.__applied_file, name)
884 unapplied.remove(name)
885 f = file(self.__unapplied_file, 'w+')
886 f.writelines([line + '\n' for line in unapplied])
887 f.close()
889 self.__set_current(name)
891 # head == bottom case doesn't need to refresh the patch
892 if empty or head != bottom:
893 if not ex:
894 # if the merge was OK and no conflicts, just refresh the patch
895 # The GIT cache was already updated by the merge operation
896 self.refresh_patch(cache_update = False)
897 else:
898 raise StackException, str(ex)
900 return modified
902 def undo_push(self):
903 name = self.get_current()
904 assert(name)
906 patch = Patch(name, self.__patch_dir, self.__refs_dir)
907 old_bottom = patch.get_old_bottom()
908 old_top = patch.get_old_top()
910 # the top of the patch is changed by a push operation only
911 # together with the bottom (otherwise the top was probably
912 # modified by 'refresh'). If they are both unchanged, there
913 # was a fast forward
914 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
915 raise StackException, 'No push undo information available'
917 git.reset()
918 self.pop_patch(name)
919 return patch.restore_old_boundaries()
921 def pop_patch(self, name):
922 """Pops the top patch from the stack
924 applied = self.get_applied()
925 applied.reverse()
926 assert(name in applied)
928 patch = Patch(name, self.__patch_dir, self.__refs_dir)
930 git.switch(patch.get_bottom())
932 # save the new applied list
933 idx = applied.index(name) + 1
935 popped = applied[:idx]
936 popped.reverse()
937 unapplied = popped + self.get_unapplied()
939 f = file(self.__unapplied_file, 'w+')
940 f.writelines([line + '\n' for line in unapplied])
941 f.close()
943 del applied[:idx]
944 applied.reverse()
946 f = file(self.__applied_file, 'w+')
947 f.writelines([line + '\n' for line in applied])
948 f.close()
950 if applied == []:
951 self.__set_current(None)
952 else:
953 self.__set_current(applied[-1])
955 self.__end_stack_check()
957 def empty_patch(self, name):
958 """Returns True if the patch is empty
960 patch = Patch(name, self.__patch_dir, self.__refs_dir)
961 bottom = patch.get_bottom()
962 top = patch.get_top()
964 if bottom == top:
965 return True
966 elif git.get_commit(top).get_tree() \
967 == git.get_commit(bottom).get_tree():
968 return True
970 return False
972 def rename_patch(self, oldname, newname):
973 applied = self.get_applied()
974 unapplied = self.get_unapplied()
976 if oldname == newname:
977 raise StackException, '"To" name and "from" name are the same'
979 if newname in applied or newname in unapplied:
980 raise StackException, 'Patch "%s" already exists' % newname
982 if oldname in unapplied:
983 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
984 unapplied[unapplied.index(oldname)] = newname
986 f = file(self.__unapplied_file, 'w+')
987 f.writelines([line + '\n' for line in unapplied])
988 f.close()
989 elif oldname in applied:
990 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
991 if oldname == self.get_current():
992 self.__set_current(newname)
994 applied[applied.index(oldname)] = newname
996 f = file(self.__applied_file, 'w+')
997 f.writelines([line + '\n' for line in applied])
998 f.close()
999 else:
1000 raise StackException, 'Unknown patch "%s"' % oldname