chiark / gitweb /
Add a 'show' command
[stgit] / stgit / stack.py
1 """Basic quilt-like functionality
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
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.
10
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.
15
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 """
20
21 import sys, os
22
23 from stgit.utils import *
24 from stgit import git, basedir
25 from stgit.config import config
26
27
28 # stack exception class
29 class StackException(Exception):
30     pass
31
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
41
42 #
43 # Functions
44 #
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
47
48 def __clean_comments(f):
49     """Removes lines marked for status in a commit file
50     """
51     f.seek(0)
52
53     # remove status-prefixed lines
54     lines = f.readlines()
55
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)]
59
60     # remove empty lines at the end
61     while len(lines) != 0 and lines[-1] == '\n':
62         del lines[-1]
63
64     f.seek(0); f.truncate()
65     f.writelines(lines)
66
67 def edit_file(series, line, comment, show_patch = True):
68     fname = '.stgit.msg'
69     tmpl = os.path.join(basedir.get(), 'patchdescr.tmpl')
70
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.'
84
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)
89
90     #Vim modeline must be near the end.
91     print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
92     f.close()
93
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
102
103     print 'Invoking the editor: "%s"...' % editor,
104     sys.stdout.flush()
105     print 'done (exit code: %d)' % os.system(editor)
106
107     f = file(fname, 'r+')
108
109     __clean_comments(f)
110     f.seek(0)
111     result = f.read()
112
113     f.close()
114     os.remove(fname)
115
116     return result
117
118 #
119 # Classes
120 #
121
122 class Patch:
123     """Basic patch implementation
124     """
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)
131
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'))
136
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)
142
143     def get_name(self):
144         return self.__name
145
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)
152
153         os.rename(olddir, self.__dir)
154         os.rename(old_ref_file, self.__top_ref_file)
155
156     def __update_top_ref(self, ref):
157         write_string(self.__top_ref_file, ref)
158
159     def update_top_ref(self):
160         top = self.get_top()
161         if top:
162             self.__update_top_ref(top)
163
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
174
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)
181
182     def get_old_bottom(self):
183         return self.__get_field('bottom.old')
184
185     def get_bottom(self):
186         return self.__get_field('bottom')
187
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)
193
194     def get_old_top(self):
195         return self.__get_field('top.old')
196
197     def get_top(self):
198         return self.__get_field('top')
199
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)
206
207     def restore_old_boundaries(self):
208         bottom = self.__get_field('bottom.old')
209         top = self.__get_field('top.old')
210
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
218
219     def get_description(self):
220         return self.__get_field('description', True)
221
222     def set_description(self, line):
223         self.__set_field('description', line, True)
224
225     def get_authname(self):
226         return self.__get_field('authname')
227
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)
235
236     def get_authemail(self):
237         return self.__get_field('authemail')
238
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)
246
247     def get_authdate(self):
248         return self.__get_field('authdate')
249
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)
254
255     def get_commname(self):
256         return self.__get_field('commname')
257
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)
265
266     def get_commemail(self):
267         return self.__get_field('commemail')
268
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)
276
277
278 class Series:
279     """Class including the operations on series
280     """
281     def __init__(self, name = None):
282         """Takes a series name as the parameter.
283         """
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
292
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)
299
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')
304
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
309
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()
315
316     def get_branch(self):
317         """Return the branch name for the Series object
318         """
319         return self.__name
320
321     def __set_current(self, name):
322         """Sets the topmost patch
323         """
324         if name:
325             write_string(self.__current_file, name)
326         else:
327             create_empty_file(self.__current_file)
328
329     def get_patch(self, name):
330         """Return a Patch object for the given name
331         """
332         return Patch(name, self.__patch_dir, self.__refs_dir)
333
334     def get_current(self):
335         """Return a Patch object representing the topmost patch
336         """
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
345
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
353
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
361
362     def get_base_file(self):
363         self.__begin_stack_check()
364         return self.__base_file
365
366     def get_protected(self):
367         return os.path.isfile(os.path.join(self.__series_dir, 'protected'))
368
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)
373
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)
378
379     def get_description(self):
380         if os.path.isfile(self.__descr_file):
381             return read_string(self.__descr_file)
382         else:
383             return ''
384
385     def __patch_is_current(self, patch):
386         return patch.get_name() == read_string(self.__current_file)
387
388     def __patch_applied(self, name):
389         """Return true if the patch exists in the applied list
390         """
391         return name in self.get_applied()
392
393     def __patch_unapplied(self, name):
394         """Return true if the patch exists in the unapplied list
395         """
396         return name in self.get_unapplied()
397
398     def __begin_stack_check(self):
399         """Save the current HEAD into .git/refs/heads/base if the stack
400         is empty
401         """
402         if len(self.get_applied()) == 0:
403             head = git.get_head()
404             write_string(self.__base_file, head)
405
406     def __end_stack_check(self):
407         """Remove .git/refs/heads/base if the stack is empty.
408         This warning should never happen
409         """
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'
413
414     def head_top_equal(self):
415         """Return true if the head and the top are the same
416         """
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()
423
424     def is_initialised(self):
425         """Checks if series is already initialised
426         """
427         return os.path.isdir(self.__patch_dir)
428
429     def init(self):
430         """Initialises the stgit series
431         """
432         bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
433
434         if self.is_initialised():
435             raise StackException, self.__patch_dir + ' already exists'
436         os.makedirs(self.__patch_dir)
437
438         if not os.path.isdir(bases_dir):
439             os.makedirs(bases_dir)
440
441         create_empty_file(self.__applied_file)
442         create_empty_file(self.__unapplied_file)
443         create_empty_file(self.__descr_file)
444         os.makedirs(os.path.join(self.__series_dir, 'patches'))
445         os.makedirs(self.__refs_dir)
446         self.__begin_stack_check()
447
448     def convert(self):
449         """Either convert to use a separate patch directory, or
450         unconvert to place the patches in the same directory with
451         series control files
452         """
453         if self.__patch_dir == self.__series_dir:
454             print 'Converting old-style to new-style...',
455             sys.stdout.flush()
456
457             self.__patch_dir = os.path.join(self.__series_dir, 'patches')
458             os.makedirs(self.__patch_dir)
459
460             for p in self.get_applied() + self.get_unapplied():
461                 src = os.path.join(self.__series_dir, p)
462                 dest = os.path.join(self.__patch_dir, p)
463                 os.rename(src, dest)
464
465             print 'done'
466
467         else:
468             print 'Converting new-style to old-style...',
469             sys.stdout.flush()
470
471             for p in self.get_applied() + self.get_unapplied():
472                 src = os.path.join(self.__patch_dir, p)
473                 dest = os.path.join(self.__series_dir, p)
474                 os.rename(src, dest)
475
476             if not os.listdir(self.__patch_dir):
477                 os.rmdir(self.__patch_dir)
478                 print 'done'
479             else:
480                 print 'Patch directory %s is not empty.' % self.__name
481
482             self.__patch_dir = self.__series_dir
483
484     def rename(self, to_name):
485         """Renames a series
486         """
487         to_stack = Series(to_name)
488
489         if to_stack.is_initialised():
490             raise StackException, '"%s" already exists' % to_stack.get_branch()
491         if os.path.exists(to_stack.__base_file):
492             os.remove(to_stack.__base_file)
493
494         git.rename_branch(self.__name, to_name)
495
496         if os.path.isdir(self.__series_dir):
497             os.rename(self.__series_dir, to_stack.__series_dir)
498         if os.path.exists(self.__base_file):
499             os.rename(self.__base_file, to_stack.__base_file)
500
501         self.__init__(to_name)
502
503     def clone(self, target_series):
504         """Clones a series
505         """
506         base = read_string(self.get_base_file())
507         git.create_branch(target_series, tree_id = base)
508         Series(target_series).init()
509         new_series = Series(target_series)
510
511         # generate an artificial description file
512         write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
513
514         # clone self's entire series as unapplied patches
515         patches = self.get_applied() + self.get_unapplied()
516         patches.reverse()
517         for p in patches:
518             patch = self.get_patch(p)
519             new_series.new_patch(p, message = patch.get_description(),
520                                  can_edit = False, unapplied = True,
521                                  bottom = patch.get_bottom(),
522                                  top = patch.get_top(),
523                                  author_name = patch.get_authname(),
524                                  author_email = patch.get_authemail(),
525                                  author_date = patch.get_authdate())
526
527         # fast forward the cloned series to self's top
528         new_series.forward_patches(self.get_applied())
529
530     def delete(self, force = False):
531         """Deletes an stgit series
532         """
533         if self.is_initialised():
534             patches = self.get_unapplied() + self.get_applied()
535             if not force and patches:
536                 raise StackException, \
537                       'Cannot delete: the series still contains patches'
538             for p in patches:
539                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
540
541             if os.path.exists(self.__applied_file):
542                 os.remove(self.__applied_file)
543             if os.path.exists(self.__unapplied_file):
544                 os.remove(self.__unapplied_file)
545             if os.path.exists(self.__current_file):
546                 os.remove(self.__current_file)
547             if os.path.exists(self.__descr_file):
548                 os.remove(self.__descr_file)
549             if not os.listdir(self.__patch_dir):
550                 os.rmdir(self.__patch_dir)
551             else:
552                 print 'Patch directory %s is not empty.' % self.__name
553             if not os.listdir(self.__series_dir):
554                 os.rmdir(self.__series_dir)
555             else:
556                 print 'Series directory %s is not empty.' % self.__name
557             if not os.listdir(self.__refs_dir):
558                 os.rmdir(self.__refs_dir)
559             else:
560                 print 'Refs directory %s is not empty.' % self.__refs_dir
561
562         if os.path.exists(self.__base_file):
563             os.remove(self.__base_file)
564
565     def refresh_patch(self, files = None, message = None, edit = False,
566                       show_patch = False,
567                       cache_update = True,
568                       author_name = None, author_email = None,
569                       author_date = None,
570                       committer_name = None, committer_email = None,
571                       backup = False):
572         """Generates a new commit for the given patch
573         """
574         name = self.get_current()
575         if not name:
576             raise StackException, 'No patches applied'
577
578         patch = Patch(name, self.__patch_dir, self.__refs_dir)
579
580         descr = patch.get_description()
581         if not (message or descr):
582             edit = True
583             descr = ''
584         elif message:
585             descr = message
586
587         if not message and edit:
588             descr = edit_file(self, descr.rstrip(), \
589                               'Please edit the description for patch "%s" ' \
590                               'above.' % name, show_patch)
591
592         if not author_name:
593             author_name = patch.get_authname()
594         if not author_email:
595             author_email = patch.get_authemail()
596         if not author_date:
597             author_date = patch.get_authdate()
598         if not committer_name:
599             committer_name = patch.get_commname()
600         if not committer_email:
601             committer_email = patch.get_commemail()
602
603         bottom = patch.get_bottom()
604
605         commit_id = git.commit(files = files,
606                                message = descr, parents = [bottom],
607                                cache_update = cache_update,
608                                allowempty = True,
609                                author_name = author_name,
610                                author_email = author_email,
611                                author_date = author_date,
612                                committer_name = committer_name,
613                                committer_email = committer_email)
614
615         patch.set_bottom(bottom, backup = backup)
616         patch.set_top(commit_id, backup = backup)
617         patch.set_description(descr)
618         patch.set_authname(author_name)
619         patch.set_authemail(author_email)
620         patch.set_authdate(author_date)
621         patch.set_commname(committer_name)
622         patch.set_commemail(committer_email)
623
624         return commit_id
625
626     def undo_refresh(self):
627         """Undo the patch boundaries changes caused by 'refresh'
628         """
629         name = self.get_current()
630         assert(name)
631
632         patch = Patch(name, self.__patch_dir, self.__refs_dir)
633         old_bottom = patch.get_old_bottom()
634         old_top = patch.get_old_top()
635
636         # the bottom of the patch is not changed by refresh. If the
637         # old_bottom is different, there wasn't any previous 'refresh'
638         # command (probably only a 'push')
639         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
640             raise StackException, 'No refresh undo information available'
641
642         git.reset(tree_id = old_top, check_out = False)
643         patch.restore_old_boundaries()
644
645     def new_patch(self, name, message = None, can_edit = True,
646                   unapplied = False, show_patch = False,
647                   top = None, bottom = None,
648                   author_name = None, author_email = None, author_date = None,
649                   committer_name = None, committer_email = None,
650                   before_existing = False):
651         """Creates a new patch
652         """
653         if self.__patch_applied(name) or self.__patch_unapplied(name):
654             raise StackException, 'Patch "%s" already exists' % name
655
656         if not message and can_edit:
657             descr = edit_file(self, None, \
658                               'Please enter the description for patch "%s" ' \
659                               'above.' % name, show_patch)
660         else:
661             descr = message
662
663         head = git.get_head()
664
665         self.__begin_stack_check()
666
667         patch = Patch(name, self.__patch_dir, self.__refs_dir)
668         patch.create()
669
670         if bottom:
671             patch.set_bottom(bottom)
672         else:
673             patch.set_bottom(head)
674         if top:
675             patch.set_top(top)
676         else:
677             patch.set_top(head)
678
679         patch.set_description(descr)
680         patch.set_authname(author_name)
681         patch.set_authemail(author_email)
682         patch.set_authdate(author_date)
683         patch.set_commname(committer_name)
684         patch.set_commemail(committer_email)
685
686         if unapplied:
687             patches = [patch.get_name()] + self.get_unapplied()
688
689             f = file(self.__unapplied_file, 'w+')
690             f.writelines([line + '\n' for line in patches])
691             f.close()
692         else:
693             if before_existing:
694                 insert_string(self.__applied_file, patch.get_name())
695                 if not self.get_current():
696                     self.__set_current(name)
697             else:
698                 append_string(self.__applied_file, patch.get_name())
699                 self.__set_current(name)
700
701     def delete_patch(self, name):
702         """Deletes a patch
703         """
704         patch = Patch(name, self.__patch_dir, self.__refs_dir)
705
706         if self.__patch_is_current(patch):
707             self.pop_patch(name)
708         elif self.__patch_applied(name):
709             raise StackException, 'Cannot remove an applied patch, "%s", ' \
710                   'which is not current' % name
711         elif not name in self.get_unapplied():
712             raise StackException, 'Unknown patch "%s"' % name
713
714         patch.delete()
715
716         unapplied = self.get_unapplied()
717         unapplied.remove(name)
718         f = file(self.__unapplied_file, 'w+')
719         f.writelines([line + '\n' for line in unapplied])
720         f.close()
721         self.__begin_stack_check()
722
723     def forward_patches(self, names):
724         """Try to fast-forward an array of patches.
725
726         On return, patches in names[0:returned_value] have been pushed on the
727         stack. Apply the rest with push_patch
728         """
729         unapplied = self.get_unapplied()
730         self.__begin_stack_check()
731
732         forwarded = 0
733         top = git.get_head()
734
735         for name in names:
736             assert(name in unapplied)
737
738             patch = Patch(name, self.__patch_dir, self.__refs_dir)
739
740             head = top
741             bottom = patch.get_bottom()
742             top = patch.get_top()
743
744             # top != bottom always since we have a commit for each patch
745             if head == bottom:
746                 # reset the backup information
747                 patch.set_bottom(head, backup = True)
748                 patch.set_top(top, backup = True)
749
750             else:
751                 head_tree = git.get_commit(head).get_tree()
752                 bottom_tree = git.get_commit(bottom).get_tree()
753                 if head_tree == bottom_tree:
754                     # We must just reparent this patch and create a new commit
755                     # for it
756                     descr = patch.get_description()
757                     author_name = patch.get_authname()
758                     author_email = patch.get_authemail()
759                     author_date = patch.get_authdate()
760                     committer_name = patch.get_commname()
761                     committer_email = patch.get_commemail()
762
763                     top_tree = git.get_commit(top).get_tree()
764
765                     top = git.commit(message = descr, parents = [head],
766                                      cache_update = False,
767                                      tree_id = top_tree,
768                                      allowempty = True,
769                                      author_name = author_name,
770                                      author_email = author_email,
771                                      author_date = author_date,
772                                      committer_name = committer_name,
773                                      committer_email = committer_email)
774
775                     patch.set_bottom(head, backup = True)
776                     patch.set_top(top, backup = True)
777                 else:
778                     top = head
779                     # stop the fast-forwarding, must do a real merge
780                     break
781
782             forwarded+=1
783             unapplied.remove(name)
784
785         if forwarded == 0:
786             return 0
787
788         git.switch(top)
789
790         append_strings(self.__applied_file, names[0:forwarded])
791
792         f = file(self.__unapplied_file, 'w+')
793         f.writelines([line + '\n' for line in unapplied])
794         f.close()
795
796         self.__set_current(name)
797
798         return forwarded
799
800     def merged_patches(self, names):
801         """Test which patches were merged upstream by reverse-applying
802         them in reverse order. The function returns the list of
803         patches detected to have been applied. The state of the tree
804         is restored to the original one
805         """
806         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
807                    for name in names]
808         patches.reverse()
809
810         merged = []
811         for p in patches:
812             if git.apply_diff(p.get_top(), p.get_bottom(), False):
813                 merged.append(p.get_name())
814         merged.reverse()
815
816         git.reset()
817
818         return merged
819
820     def push_patch(self, name, empty = False):
821         """Pushes a patch on the stack
822         """
823         unapplied = self.get_unapplied()
824         assert(name in unapplied)
825
826         self.__begin_stack_check()
827
828         patch = Patch(name, self.__patch_dir, self.__refs_dir)
829
830         head = git.get_head()
831         bottom = patch.get_bottom()
832         top = patch.get_top()
833
834         ex = None
835         modified = False
836
837         # top != bottom always since we have a commit for each patch
838         if empty:
839             # just make an empty patch (top = bottom = HEAD). This
840             # option is useful to allow undoing already merged
841             # patches. The top is updated by refresh_patch since we
842             # need an empty commit
843             patch.set_bottom(head, backup = True)
844             patch.set_top(head, backup = True)
845             modified = True
846         elif head == bottom:
847             # reset the backup information
848             patch.set_bottom(bottom, backup = True)
849             patch.set_top(top, backup = True)
850
851             git.switch(top)
852         else:
853             # new patch needs to be refreshed.
854             # The current patch is empty after merge.
855             patch.set_bottom(head, backup = True)
856             patch.set_top(head, backup = True)
857
858             # Try the fast applying first. If this fails, fall back to the
859             # three-way merge
860             if not git.apply_diff(bottom, top):
861                 # if git.apply_diff() fails, the patch requires a diff3
862                 # merge and can be reported as modified
863                 modified = True
864
865                 # merge can fail but the patch needs to be pushed
866                 try:
867                     git.merge(bottom, head, top)
868                 except git.GitException, ex:
869                     print >> sys.stderr, \
870                           'The merge failed during "push". ' \
871                           'Use "refresh" after fixing the conflicts'
872
873         append_string(self.__applied_file, name)
874
875         unapplied.remove(name)
876         f = file(self.__unapplied_file, 'w+')
877         f.writelines([line + '\n' for line in unapplied])
878         f.close()
879
880         self.__set_current(name)
881
882         # head == bottom case doesn't need to refresh the patch
883         if empty or head != bottom:
884             if not ex:
885                 # if the merge was OK and no conflicts, just refresh the patch
886                 # The GIT cache was already updated by the merge operation
887                 self.refresh_patch(cache_update = False)
888             else:
889                 raise StackException, str(ex)
890
891         return modified
892
893     def undo_push(self):
894         name = self.get_current()
895         assert(name)
896
897         patch = Patch(name, self.__patch_dir, self.__refs_dir)
898         old_bottom = patch.get_old_bottom()
899         old_top = patch.get_old_top()
900
901         # the top of the patch is changed by a push operation only
902         # together with the bottom (otherwise the top was probably
903         # modified by 'refresh'). If they are both unchanged, there
904         # was a fast forward
905         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
906             raise StackException, 'No push undo information available'
907
908         git.reset()
909         self.pop_patch(name)
910         return patch.restore_old_boundaries()
911
912     def pop_patch(self, name):
913         """Pops the top patch from the stack
914         """
915         applied = self.get_applied()
916         applied.reverse()
917         assert(name in applied)
918
919         patch = Patch(name, self.__patch_dir, self.__refs_dir)
920
921         git.switch(patch.get_bottom())
922
923         # save the new applied list
924         idx = applied.index(name) + 1
925
926         popped = applied[:idx]
927         popped.reverse()
928         unapplied = popped + self.get_unapplied()
929
930         f = file(self.__unapplied_file, 'w+')
931         f.writelines([line + '\n' for line in unapplied])
932         f.close()
933
934         del applied[:idx]
935         applied.reverse()
936
937         f = file(self.__applied_file, 'w+')
938         f.writelines([line + '\n' for line in applied])
939         f.close()
940
941         if applied == []:
942             self.__set_current(None)
943         else:
944             self.__set_current(applied[-1])
945
946         self.__end_stack_check()
947
948     def empty_patch(self, name):
949         """Returns True if the patch is empty
950         """
951         patch = Patch(name, self.__patch_dir, self.__refs_dir)
952         bottom = patch.get_bottom()
953         top = patch.get_top()
954
955         if bottom == top:
956             return True
957         elif git.get_commit(top).get_tree() \
958                  == git.get_commit(bottom).get_tree():
959             return True
960
961         return False
962
963     def rename_patch(self, oldname, newname):
964         applied = self.get_applied()
965         unapplied = self.get_unapplied()
966
967         if oldname == newname:
968             raise StackException, '"To" name and "from" name are the same'
969
970         if newname in applied or newname in unapplied:
971             raise StackException, 'Patch "%s" already exists' % newname
972
973         if oldname in unapplied:
974             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
975             unapplied[unapplied.index(oldname)] = newname
976
977             f = file(self.__unapplied_file, 'w+')
978             f.writelines([line + '\n' for line in unapplied])
979             f.close()
980         elif oldname in applied:
981             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
982             if oldname == self.get_current():
983                 self.__set_current(newname)
984
985             applied[applied.index(oldname)] = newname
986
987             f = file(self.__applied_file, 'w+')
988             f.writelines([line + '\n' for line in applied])
989             f.close()
990         else:
991             raise StackException, 'Unknown patch "%s"' % oldname