Add support to hide and unhide patches
[stgit.git] / stgit / stack.py
blob5237084ce04bf00e893599df0aacbb7a9ff0840b
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, templates
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 = '.stgitmsg.txt'
69 tmpl = templates.get_template('patchdescr.tmpl')
71 f = file(fname, 'w+')
72 if line:
73 print >> f, line
74 elif tmpl:
75 print >> f, tmpl,
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 StgitObject:
123 """An object with stgit-like properties stored as files in a directory
125 def _set_dir(self, dir):
126 self.__dir = dir
127 def _dir(self):
128 return self.__dir
130 def create_empty_field(self, name):
131 create_empty_file(os.path.join(self.__dir, name))
133 def _get_field(self, name, multiline = False):
134 id_file = os.path.join(self.__dir, name)
135 if os.path.isfile(id_file):
136 line = read_string(id_file, multiline)
137 if line == '':
138 return None
139 else:
140 return line
141 else:
142 return None
144 def _set_field(self, name, value, multiline = False):
145 fname = os.path.join(self.__dir, name)
146 if value and value != '':
147 write_string(fname, value, multiline)
148 elif os.path.isfile(fname):
149 os.remove(fname)
152 class Patch(StgitObject):
153 """Basic patch implementation
155 def __init__(self, name, series_dir, refs_dir):
156 self.__series_dir = series_dir
157 self.__name = name
158 self._set_dir(os.path.join(self.__series_dir, self.__name))
159 self.__refs_dir = refs_dir
160 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
161 self.__log_ref_file = os.path.join(self.__refs_dir,
162 self.__name + '.log')
164 def create(self):
165 os.mkdir(self._dir())
166 self.create_empty_field('bottom')
167 self.create_empty_field('top')
169 def delete(self):
170 for f in os.listdir(self._dir()):
171 os.remove(os.path.join(self._dir(), f))
172 os.rmdir(self._dir())
173 os.remove(self.__top_ref_file)
174 if os.path.exists(self.__log_ref_file):
175 os.remove(self.__log_ref_file)
177 def get_name(self):
178 return self.__name
180 def rename(self, newname):
181 olddir = self._dir()
182 old_top_ref_file = self.__top_ref_file
183 old_log_ref_file = self.__log_ref_file
184 self.__name = newname
185 self._set_dir(os.path.join(self.__series_dir, self.__name))
186 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
187 self.__log_ref_file = os.path.join(self.__refs_dir,
188 self.__name + '.log')
190 os.rename(olddir, self._dir())
191 os.rename(old_top_ref_file, self.__top_ref_file)
192 if os.path.exists(old_log_ref_file):
193 os.rename(old_log_ref_file, self.__log_ref_file)
195 def __update_top_ref(self, ref):
196 write_string(self.__top_ref_file, ref)
198 def __update_log_ref(self, ref):
199 write_string(self.__log_ref_file, ref)
201 def update_top_ref(self):
202 top = self.get_top()
203 if top:
204 self.__update_top_ref(top)
206 def get_old_bottom(self):
207 return self._get_field('bottom.old')
209 def get_bottom(self):
210 return self._get_field('bottom')
212 def set_bottom(self, value, backup = False):
213 if backup:
214 curr = self._get_field('bottom')
215 self._set_field('bottom.old', curr)
216 self._set_field('bottom', value)
218 def get_old_top(self):
219 return self._get_field('top.old')
221 def get_top(self):
222 return self._get_field('top')
224 def set_top(self, value, backup = False):
225 if backup:
226 curr = self._get_field('top')
227 self._set_field('top.old', curr)
228 self._set_field('top', value)
229 self.__update_top_ref(value)
231 def restore_old_boundaries(self):
232 bottom = self._get_field('bottom.old')
233 top = self._get_field('top.old')
235 if top and bottom:
236 self._set_field('bottom', bottom)
237 self._set_field('top', top)
238 self.__update_top_ref(top)
239 return True
240 else:
241 return False
243 def get_description(self):
244 return self._get_field('description', True)
246 def set_description(self, line):
247 self._set_field('description', line, True)
249 def get_authname(self):
250 return self._get_field('authname')
252 def set_authname(self, name):
253 self._set_field('authname', name or git.author().name)
255 def get_authemail(self):
256 return self._get_field('authemail')
258 def set_authemail(self, email):
259 self._set_field('authemail', email or git.author().email)
261 def get_authdate(self):
262 return self._get_field('authdate')
264 def set_authdate(self, date):
265 self._set_field('authdate', date or git.author().date)
267 def get_commname(self):
268 return self._get_field('commname')
270 def set_commname(self, name):
271 self._set_field('commname', name or git.committer().name)
273 def get_commemail(self):
274 return self._get_field('commemail')
276 def set_commemail(self, email):
277 self._set_field('commemail', email or git.committer().email)
279 def get_log(self):
280 return self._get_field('log')
282 def set_log(self, value, backup = False):
283 self._set_field('log', value)
284 self.__update_log_ref(value)
287 class Series(StgitObject):
288 """Class including the operations on series
290 def __init__(self, name = None):
291 """Takes a series name as the parameter.
293 try:
294 if name:
295 self.__name = name
296 else:
297 self.__name = git.get_head_file()
298 self.__base_dir = basedir.get()
299 except git.GitException, ex:
300 raise StackException, 'GIT tree not initialised: %s' % ex
302 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
303 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
304 self.__name)
305 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
306 self.__name)
308 self.__applied_file = os.path.join(self._dir(), 'applied')
309 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
310 self.__hidden_file = os.path.join(self._dir(), 'hidden')
311 self.__current_file = os.path.join(self._dir(), 'current')
312 self.__descr_file = os.path.join(self._dir(), 'description')
314 # where this series keeps its patches
315 self.__patch_dir = os.path.join(self._dir(), 'patches')
316 if not os.path.isdir(self.__patch_dir):
317 self.__patch_dir = self._dir()
319 # if no __refs_dir, create and populate it (upgrade old repositories)
320 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
321 os.makedirs(self.__refs_dir)
322 for patch in self.get_applied() + self.get_unapplied():
323 self.get_patch(patch).update_top_ref()
325 # trash directory
326 self.__trash_dir = os.path.join(self._dir(), 'trash')
327 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
328 os.makedirs(self.__trash_dir)
330 def get_branch(self):
331 """Return the branch name for the Series object
333 return self.__name
335 def __set_current(self, name):
336 """Sets the topmost patch
338 self._set_field('current', name)
340 def get_patch(self, name):
341 """Return a Patch object for the given name
343 return Patch(name, self.__patch_dir, self.__refs_dir)
345 def get_current_patch(self):
346 """Return a Patch object representing the topmost patch, or
347 None if there is no such patch."""
348 crt = self.get_current()
349 if not crt:
350 return None
351 return Patch(crt, self.__patch_dir, self.__refs_dir)
353 def get_current(self):
354 """Return the name of the topmost patch, or None if there is
355 no such patch."""
356 name = self._get_field('current')
357 if name == '':
358 return None
359 else:
360 return name
362 def get_applied(self):
363 if not os.path.isfile(self.__applied_file):
364 raise StackException, 'Branch "%s" not initialised' % self.__name
365 f = file(self.__applied_file)
366 names = [line.strip() for line in f.readlines()]
367 f.close()
368 return names
370 def get_unapplied(self):
371 if not os.path.isfile(self.__unapplied_file):
372 raise StackException, 'Branch "%s" not initialised' % self.__name
373 f = file(self.__unapplied_file)
374 names = [line.strip() for line in f.readlines()]
375 f.close()
376 return names
378 def get_hidden(self):
379 if not os.path.isfile(self.__hidden_file):
380 return []
381 f = file(self.__hidden_file)
382 names = [line.strip() for line in f.readlines()]
383 f.close()
384 return names
386 def get_base_file(self):
387 self.__begin_stack_check()
388 return self.__base_file
390 def get_protected(self):
391 return os.path.isfile(os.path.join(self._dir(), 'protected'))
393 def protect(self):
394 protect_file = os.path.join(self._dir(), 'protected')
395 if not os.path.isfile(protect_file):
396 create_empty_file(protect_file)
398 def unprotect(self):
399 protect_file = os.path.join(self._dir(), 'protected')
400 if os.path.isfile(protect_file):
401 os.remove(protect_file)
403 def get_description(self):
404 return self._get_field('description') or ''
406 def set_description(self, line):
407 self._set_field('description', line)
409 def __patch_is_current(self, patch):
410 return patch.get_name() == self.get_current()
412 def patch_applied(self, name):
413 """Return true if the patch exists in the applied list
415 return name in self.get_applied()
417 def patch_unapplied(self, name):
418 """Return true if the patch exists in the unapplied list
420 return name in self.get_unapplied()
422 def patch_hidden(self, name):
423 """Return true if the patch is hidden.
425 return name in self.get_hidden()
427 def patch_exists(self, name):
428 """Return true if there is a patch with the given name, false
429 otherwise."""
430 return self.patch_applied(name) or self.patch_unapplied(name)
432 def __begin_stack_check(self):
433 """Save the current HEAD into .git/refs/heads/base if the stack
434 is empty
436 if len(self.get_applied()) == 0:
437 head = git.get_head()
438 write_string(self.__base_file, head)
440 def __end_stack_check(self):
441 """Remove .git/refs/heads/base if the stack is empty.
442 This warning should never happen
444 if len(self.get_applied()) == 0 \
445 and read_string(self.__base_file) != git.get_head():
446 print 'Warning: stack empty but the HEAD and base are different'
448 def head_top_equal(self):
449 """Return true if the head and the top are the same
451 crt = self.get_current_patch()
452 if not crt:
453 # we don't care, no patches applied
454 return True
455 return git.get_head() == crt.get_top()
457 def is_initialised(self):
458 """Checks if series is already initialised
460 return os.path.isdir(self.__patch_dir)
462 def init(self, create_at=False):
463 """Initialises the stgit series
465 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
467 if os.path.exists(self.__patch_dir):
468 raise StackException, self.__patch_dir + ' already exists'
469 if os.path.exists(self.__refs_dir):
470 raise StackException, self.__refs_dir + ' already exists'
471 if os.path.exists(self.__base_file):
472 raise StackException, self.__base_file + ' already exists'
474 if (create_at!=False):
475 git.create_branch(self.__name, create_at)
477 os.makedirs(self.__patch_dir)
479 create_dirs(bases_dir)
481 self.create_empty_field('applied')
482 self.create_empty_field('unapplied')
483 self.create_empty_field('description')
484 os.makedirs(os.path.join(self._dir(), 'patches'))
485 os.makedirs(self.__refs_dir)
486 self.__begin_stack_check()
488 def convert(self):
489 """Either convert to use a separate patch directory, or
490 unconvert to place the patches in the same directory with
491 series control files
493 if self.__patch_dir == self._dir():
494 print 'Converting old-style to new-style...',
495 sys.stdout.flush()
497 self.__patch_dir = os.path.join(self._dir(), 'patches')
498 os.makedirs(self.__patch_dir)
500 for p in self.get_applied() + self.get_unapplied():
501 src = os.path.join(self._dir(), p)
502 dest = os.path.join(self.__patch_dir, p)
503 os.rename(src, dest)
505 print 'done'
507 else:
508 print 'Converting new-style to old-style...',
509 sys.stdout.flush()
511 for p in self.get_applied() + self.get_unapplied():
512 src = os.path.join(self.__patch_dir, p)
513 dest = os.path.join(self._dir(), p)
514 os.rename(src, dest)
516 if not os.listdir(self.__patch_dir):
517 os.rmdir(self.__patch_dir)
518 print 'done'
519 else:
520 print 'Patch directory %s is not empty.' % self.__name
522 self.__patch_dir = self._dir()
524 def rename(self, to_name):
525 """Renames a series
527 to_stack = Series(to_name)
529 if to_stack.is_initialised():
530 raise StackException, '"%s" already exists' % to_stack.get_branch()
531 if os.path.exists(to_stack.__base_file):
532 os.remove(to_stack.__base_file)
534 git.rename_branch(self.__name, to_name)
536 if os.path.isdir(self._dir()):
537 rename(os.path.join(self.__base_dir, 'patches'),
538 self.__name, to_stack.__name)
539 if os.path.exists(self.__base_file):
540 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
541 self.__name, to_stack.__name)
542 if os.path.exists(self.__refs_dir):
543 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
544 self.__name, to_stack.__name)
546 self.__init__(to_name)
548 def clone(self, target_series):
549 """Clones a series
551 try:
552 # allow cloning of branches not under StGIT control
553 base = read_string(self.get_base_file())
554 except:
555 base = git.get_head()
556 Series(target_series).init(create_at = base)
557 new_series = Series(target_series)
559 # generate an artificial description file
560 new_series.set_description('clone of "%s"' % self.__name)
562 # clone self's entire series as unapplied patches
563 try:
564 # allow cloning of branches not under StGIT control
565 applied = self.get_applied()
566 unapplied = self.get_unapplied()
567 patches = applied + unapplied
568 patches.reverse()
569 except:
570 patches = applied = unapplied = []
571 for p in patches:
572 patch = self.get_patch(p)
573 new_series.new_patch(p, message = patch.get_description(),
574 can_edit = False, unapplied = True,
575 bottom = patch.get_bottom(),
576 top = patch.get_top(),
577 author_name = patch.get_authname(),
578 author_email = patch.get_authemail(),
579 author_date = patch.get_authdate())
581 # fast forward the cloned series to self's top
582 new_series.forward_patches(applied)
584 def delete(self, force = False):
585 """Deletes an stgit series
587 if self.is_initialised():
588 patches = self.get_unapplied() + self.get_applied()
589 if not force and patches:
590 raise StackException, \
591 'Cannot delete: the series still contains patches'
592 for p in patches:
593 Patch(p, self.__patch_dir, self.__refs_dir).delete()
595 # remove the trash directory
596 for fname in os.listdir(self.__trash_dir):
597 os.remove(fname)
598 os.rmdir(self.__trash_dir)
600 # FIXME: find a way to get rid of those manual removals
601 # (move functionnality to StgitObject ?)
602 if os.path.exists(self.__applied_file):
603 os.remove(self.__applied_file)
604 if os.path.exists(self.__unapplied_file):
605 os.remove(self.__unapplied_file)
606 if os.path.exists(self.__hidden_file):
607 os.remove(self.__hidden_file)
608 if os.path.exists(self.__current_file):
609 os.remove(self.__current_file)
610 if os.path.exists(self.__descr_file):
611 os.remove(self.__descr_file)
612 if not os.listdir(self.__patch_dir):
613 os.rmdir(self.__patch_dir)
614 else:
615 print 'Patch directory %s is not empty.' % self.__name
616 if not os.listdir(self._dir()):
617 remove_dirs(os.path.join(self.__base_dir, 'patches'),
618 self.__name)
619 else:
620 print 'Series directory %s is not empty.' % self.__name
621 if not os.listdir(self.__refs_dir):
622 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
623 self.__name)
624 else:
625 print 'Refs directory %s is not empty.' % self.__refs_dir
627 if os.path.exists(self.__base_file):
628 remove_file_and_dirs(
629 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
631 def refresh_patch(self, files = None, message = None, edit = False,
632 show_patch = False,
633 cache_update = True,
634 author_name = None, author_email = None,
635 author_date = None,
636 committer_name = None, committer_email = None,
637 backup = False, sign_str = None, log = 'refresh'):
638 """Generates a new commit for the given patch
640 name = self.get_current()
641 if not name:
642 raise StackException, 'No patches applied'
644 patch = Patch(name, self.__patch_dir, self.__refs_dir)
646 descr = patch.get_description()
647 if not (message or descr):
648 edit = True
649 descr = ''
650 elif message:
651 descr = message
653 if not message and edit:
654 descr = edit_file(self, descr.rstrip(), \
655 'Please edit the description for patch "%s" ' \
656 'above.' % name, show_patch)
658 if not author_name:
659 author_name = patch.get_authname()
660 if not author_email:
661 author_email = patch.get_authemail()
662 if not author_date:
663 author_date = patch.get_authdate()
664 if not committer_name:
665 committer_name = patch.get_commname()
666 if not committer_email:
667 committer_email = patch.get_commemail()
669 if sign_str:
670 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
671 committer_name, committer_email)
673 bottom = patch.get_bottom()
675 commit_id = git.commit(files = files,
676 message = descr, parents = [bottom],
677 cache_update = cache_update,
678 allowempty = True,
679 author_name = author_name,
680 author_email = author_email,
681 author_date = author_date,
682 committer_name = committer_name,
683 committer_email = committer_email)
685 patch.set_bottom(bottom, backup = backup)
686 patch.set_top(commit_id, backup = backup)
687 patch.set_description(descr)
688 patch.set_authname(author_name)
689 patch.set_authemail(author_email)
690 patch.set_authdate(author_date)
691 patch.set_commname(committer_name)
692 patch.set_commemail(committer_email)
694 if log:
695 self.log_patch(patch, log)
697 return commit_id
699 def undo_refresh(self):
700 """Undo the patch boundaries changes caused by 'refresh'
702 name = self.get_current()
703 assert(name)
705 patch = Patch(name, self.__patch_dir, self.__refs_dir)
706 old_bottom = patch.get_old_bottom()
707 old_top = patch.get_old_top()
709 # the bottom of the patch is not changed by refresh. If the
710 # old_bottom is different, there wasn't any previous 'refresh'
711 # command (probably only a 'push')
712 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
713 raise StackException, 'No undo information available'
715 git.reset(tree_id = old_top, check_out = False)
716 if patch.restore_old_boundaries():
717 self.log_patch(patch, 'undo')
719 def new_patch(self, name, message = None, can_edit = True,
720 unapplied = False, show_patch = False,
721 top = None, bottom = None,
722 author_name = None, author_email = None, author_date = None,
723 committer_name = None, committer_email = None,
724 before_existing = False, refresh = True):
725 """Creates a new patch
727 if self.patch_applied(name) or self.patch_unapplied(name):
728 raise StackException, 'Patch "%s" already exists' % name
730 if not message and can_edit:
731 descr = edit_file(self, None, \
732 'Please enter the description for patch "%s" ' \
733 'above.' % name, show_patch)
734 else:
735 descr = message
737 head = git.get_head()
739 self.__begin_stack_check()
741 patch = Patch(name, self.__patch_dir, self.__refs_dir)
742 patch.create()
744 if bottom:
745 patch.set_bottom(bottom)
746 else:
747 patch.set_bottom(head)
748 if top:
749 patch.set_top(top)
750 else:
751 patch.set_top(head)
753 patch.set_description(descr)
754 patch.set_authname(author_name)
755 patch.set_authemail(author_email)
756 patch.set_authdate(author_date)
757 patch.set_commname(committer_name)
758 patch.set_commemail(committer_email)
760 if unapplied:
761 self.log_patch(patch, 'new')
763 patches = [patch.get_name()] + self.get_unapplied()
765 f = file(self.__unapplied_file, 'w+')
766 f.writelines([line + '\n' for line in patches])
767 f.close()
768 elif before_existing:
769 self.log_patch(patch, 'new')
771 insert_string(self.__applied_file, patch.get_name())
772 if not self.get_current():
773 self.__set_current(name)
774 else:
775 append_string(self.__applied_file, patch.get_name())
776 self.__set_current(name)
777 if refresh:
778 self.refresh_patch(cache_update = False, log = 'new')
780 def delete_patch(self, name):
781 """Deletes a patch
783 patch = Patch(name, self.__patch_dir, self.__refs_dir)
785 if self.__patch_is_current(patch):
786 self.pop_patch(name)
787 elif self.patch_applied(name):
788 raise StackException, 'Cannot remove an applied patch, "%s", ' \
789 'which is not current' % name
790 elif not name in self.get_unapplied():
791 raise StackException, 'Unknown patch "%s"' % name
793 # save the commit id to a trash file
794 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
796 patch.delete()
798 unapplied = self.get_unapplied()
799 unapplied.remove(name)
800 f = file(self.__unapplied_file, 'w+')
801 f.writelines([line + '\n' for line in unapplied])
802 f.close()
804 if self.patch_hidden(name):
805 self.unhide_patch(name)
807 self.__begin_stack_check()
809 def forward_patches(self, names):
810 """Try to fast-forward an array of patches.
812 On return, patches in names[0:returned_value] have been pushed on the
813 stack. Apply the rest with push_patch
815 unapplied = self.get_unapplied()
816 self.__begin_stack_check()
818 forwarded = 0
819 top = git.get_head()
821 for name in names:
822 assert(name in unapplied)
824 patch = Patch(name, self.__patch_dir, self.__refs_dir)
826 head = top
827 bottom = patch.get_bottom()
828 top = patch.get_top()
830 # top != bottom always since we have a commit for each patch
831 if head == bottom:
832 # reset the backup information. No logging since the
833 # patch hasn't changed
834 patch.set_bottom(head, backup = True)
835 patch.set_top(top, backup = True)
837 else:
838 head_tree = git.get_commit(head).get_tree()
839 bottom_tree = git.get_commit(bottom).get_tree()
840 if head_tree == bottom_tree:
841 # We must just reparent this patch and create a new commit
842 # for it
843 descr = patch.get_description()
844 author_name = patch.get_authname()
845 author_email = patch.get_authemail()
846 author_date = patch.get_authdate()
847 committer_name = patch.get_commname()
848 committer_email = patch.get_commemail()
850 top_tree = git.get_commit(top).get_tree()
852 top = git.commit(message = descr, parents = [head],
853 cache_update = False,
854 tree_id = top_tree,
855 allowempty = True,
856 author_name = author_name,
857 author_email = author_email,
858 author_date = author_date,
859 committer_name = committer_name,
860 committer_email = committer_email)
862 patch.set_bottom(head, backup = True)
863 patch.set_top(top, backup = True)
865 self.log_patch(patch, 'push(f)')
866 else:
867 top = head
868 # stop the fast-forwarding, must do a real merge
869 break
871 forwarded+=1
872 unapplied.remove(name)
874 if forwarded == 0:
875 return 0
877 git.switch(top)
879 append_strings(self.__applied_file, names[0:forwarded])
881 f = file(self.__unapplied_file, 'w+')
882 f.writelines([line + '\n' for line in unapplied])
883 f.close()
885 self.__set_current(name)
887 return forwarded
889 def merged_patches(self, names):
890 """Test which patches were merged upstream by reverse-applying
891 them in reverse order. The function returns the list of
892 patches detected to have been applied. The state of the tree
893 is restored to the original one
895 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
896 for name in names]
897 patches.reverse()
899 merged = []
900 for p in patches:
901 if git.apply_diff(p.get_top(), p.get_bottom()):
902 merged.append(p.get_name())
903 merged.reverse()
905 git.reset()
907 return merged
909 def push_patch(self, name, empty = False):
910 """Pushes a patch on the stack
912 unapplied = self.get_unapplied()
913 assert(name in unapplied)
915 self.__begin_stack_check()
917 patch = Patch(name, self.__patch_dir, self.__refs_dir)
919 head = git.get_head()
920 bottom = patch.get_bottom()
921 top = patch.get_top()
923 ex = None
924 modified = False
926 # top != bottom always since we have a commit for each patch
927 if empty:
928 # just make an empty patch (top = bottom = HEAD). This
929 # option is useful to allow undoing already merged
930 # patches. The top is updated by refresh_patch since we
931 # need an empty commit
932 patch.set_bottom(head, backup = True)
933 patch.set_top(head, backup = True)
934 modified = True
935 elif head == bottom:
936 # reset the backup information. No need for logging
937 patch.set_bottom(bottom, backup = True)
938 patch.set_top(top, backup = True)
940 git.switch(top)
941 else:
942 # new patch needs to be refreshed.
943 # The current patch is empty after merge.
944 patch.set_bottom(head, backup = True)
945 patch.set_top(head, backup = True)
947 # Try the fast applying first. If this fails, fall back to the
948 # three-way merge
949 if not git.apply_diff(bottom, top):
950 # if git.apply_diff() fails, the patch requires a diff3
951 # merge and can be reported as modified
952 modified = True
954 # merge can fail but the patch needs to be pushed
955 try:
956 git.merge(bottom, head, top, recursive = True)
957 except git.GitException, ex:
958 print >> sys.stderr, \
959 'The merge failed during "push". ' \
960 'Use "refresh" after fixing the conflicts'
962 append_string(self.__applied_file, name)
964 unapplied.remove(name)
965 f = file(self.__unapplied_file, 'w+')
966 f.writelines([line + '\n' for line in unapplied])
967 f.close()
969 self.__set_current(name)
971 # head == bottom case doesn't need to refresh the patch
972 if empty or head != bottom:
973 if not ex:
974 # if the merge was OK and no conflicts, just refresh the patch
975 # The GIT cache was already updated by the merge operation
976 if modified:
977 log = 'push(m)'
978 else:
979 log = 'push'
980 self.refresh_patch(cache_update = False, log = log)
981 else:
982 # we store the correctly merged files only for
983 # tracking the conflict history. Note that the
984 # git.merge() operations shouls always leave the index
985 # in a valid state (i.e. only stage 0 files)
986 self.refresh_patch(cache_update = False, log = 'push(c)')
987 raise StackException, str(ex)
989 return modified
991 def undo_push(self):
992 name = self.get_current()
993 assert(name)
995 patch = Patch(name, self.__patch_dir, self.__refs_dir)
996 old_bottom = patch.get_old_bottom()
997 old_top = patch.get_old_top()
999 # the top of the patch is changed by a push operation only
1000 # together with the bottom (otherwise the top was probably
1001 # modified by 'refresh'). If they are both unchanged, there
1002 # was a fast forward
1003 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1004 raise StackException, 'No undo information available'
1006 git.reset()
1007 self.pop_patch(name)
1008 ret = patch.restore_old_boundaries()
1009 if ret:
1010 self.log_patch(patch, 'undo')
1012 return ret
1014 def pop_patch(self, name, keep = False):
1015 """Pops the top patch from the stack
1017 applied = self.get_applied()
1018 applied.reverse()
1019 assert(name in applied)
1021 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1023 # only keep the local changes
1024 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1025 raise StackException, \
1026 'Failed to pop patches while preserving the local changes'
1028 git.switch(patch.get_bottom(), keep)
1030 # save the new applied list
1031 idx = applied.index(name) + 1
1033 popped = applied[:idx]
1034 popped.reverse()
1035 unapplied = popped + self.get_unapplied()
1037 f = file(self.__unapplied_file, 'w+')
1038 f.writelines([line + '\n' for line in unapplied])
1039 f.close()
1041 del applied[:idx]
1042 applied.reverse()
1044 f = file(self.__applied_file, 'w+')
1045 f.writelines([line + '\n' for line in applied])
1046 f.close()
1048 if applied == []:
1049 self.__set_current(None)
1050 else:
1051 self.__set_current(applied[-1])
1053 self.__end_stack_check()
1055 def empty_patch(self, name):
1056 """Returns True if the patch is empty
1058 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1059 bottom = patch.get_bottom()
1060 top = patch.get_top()
1062 if bottom == top:
1063 return True
1064 elif git.get_commit(top).get_tree() \
1065 == git.get_commit(bottom).get_tree():
1066 return True
1068 return False
1070 def rename_patch(self, oldname, newname):
1071 applied = self.get_applied()
1072 unapplied = self.get_unapplied()
1074 if oldname == newname:
1075 raise StackException, '"To" name and "from" name are the same'
1077 if newname in applied or newname in unapplied:
1078 raise StackException, 'Patch "%s" already exists' % newname
1080 if self.patch_hidden(oldname):
1081 self.unhide_patch(oldname)
1082 self.hide_patch(newname)
1084 if oldname in unapplied:
1085 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1086 unapplied[unapplied.index(oldname)] = newname
1088 f = file(self.__unapplied_file, 'w+')
1089 f.writelines([line + '\n' for line in unapplied])
1090 f.close()
1091 elif oldname in applied:
1092 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1093 if oldname == self.get_current():
1094 self.__set_current(newname)
1096 applied[applied.index(oldname)] = newname
1098 f = file(self.__applied_file, 'w+')
1099 f.writelines([line + '\n' for line in applied])
1100 f.close()
1101 else:
1102 raise StackException, 'Unknown patch "%s"' % oldname
1104 def log_patch(self, patch, message):
1105 """Generate a log commit for a patch
1107 top = git.get_commit(patch.get_top())
1108 msg = '%s\t%s' % (message, top.get_id_hash())
1110 old_log = patch.get_log()
1111 if old_log:
1112 parents = [old_log]
1113 else:
1114 parents = []
1116 log = git.commit(message = msg, parents = parents,
1117 cache_update = False, tree_id = top.get_tree(),
1118 allowempty = True)
1119 patch.set_log(log)
1121 def hide_patch(self, name):
1122 """Add the patch to the hidden list.
1124 if not self.patch_exists(name):
1125 raise StackException, 'Unknown patch "%s"' % name
1126 elif self.patch_hidden(name):
1127 raise StackException, 'Patch "%s" already hidden' % name
1129 append_string(self.__hidden_file, name)
1131 def unhide_patch(self, name):
1132 """Add the patch to the hidden list.
1134 if not self.patch_exists(name):
1135 raise StackException, 'Unknown patch "%s"' % name
1136 hidden = self.get_hidden()
1137 if not name in hidden:
1138 raise StackException, 'Patch "%s" not hidden' % name
1140 hidden.remove(name)
1142 f = file(self.__hidden_file, 'w+')
1143 f.writelines([line + '\n' for line in hidden])
1144 f.close()