chiark / gitweb /
19bb62abc57cc14b1f782ec8f2584af93343117d
[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, create_at=False):
430         """Initialises the stgit series
431         """
432         bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
433
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'
440
441         if (create_at!=False):
442             git.create_branch(self.__name, create_at)
443
444         os.makedirs(self.__patch_dir)
445
446         if not os.path.isdir(bases_dir):
447             os.makedirs(bases_dir)
448
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()
455
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
460         """
461         if self.__patch_dir == self.__series_dir:
462             print 'Converting old-style to new-style...',
463             sys.stdout.flush()
464
465             self.__patch_dir = os.path.join(self.__series_dir, 'patches')
466             os.makedirs(self.__patch_dir)
467
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)
472
473             print 'done'
474
475         else:
476             print 'Converting new-style to old-style...',
477             sys.stdout.flush()
478
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)
483
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
489
490             self.__patch_dir = self.__series_dir
491
492     def rename(self, to_name):
493         """Renames a series
494         """
495         to_stack = Series(to_name)
496
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)
501
502         git.rename_branch(self.__name, to_name)
503
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
509         self.__init__(to_name)
510
511     def clone(self, target_series):
512         """Clones a series
513         """
514         base = read_string(self.get_base_file())
515         Series(target_series).init(create_at = base)
516         new_series = Series(target_series)
517
518         # generate an artificial description file
519         write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
520
521         # clone self's entire series as unapplied patches
522         patches = self.get_applied() + self.get_unapplied()
523         patches.reverse()
524         for p in patches:
525             patch = self.get_patch(p)
526             new_series.new_patch(p, message = patch.get_description(),
527                                  can_edit = False, unapplied = True,
528                                  bottom = patch.get_bottom(),
529                                  top = patch.get_top(),
530                                  author_name = patch.get_authname(),
531                                  author_email = patch.get_authemail(),
532                                  author_date = patch.get_authdate())
533
534         # fast forward the cloned series to self's top
535         new_series.forward_patches(self.get_applied())
536
537     def delete(self, force = False):
538         """Deletes an stgit series
539         """
540         if self.is_initialised():
541             patches = self.get_unapplied() + self.get_applied()
542             if not force and patches:
543                 raise StackException, \
544                       'Cannot delete: the series still contains patches'
545             for p in patches:
546                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
547
548             if os.path.exists(self.__applied_file):
549                 os.remove(self.__applied_file)
550             if os.path.exists(self.__unapplied_file):
551                 os.remove(self.__unapplied_file)
552             if os.path.exists(self.__current_file):
553                 os.remove(self.__current_file)
554             if os.path.exists(self.__descr_file):
555                 os.remove(self.__descr_file)
556             if not os.listdir(self.__patch_dir):
557                 os.rmdir(self.__patch_dir)
558             else:
559                 print 'Patch directory %s is not empty.' % self.__name
560             if not os.listdir(self.__series_dir):
561                 os.rmdir(self.__series_dir)
562             else:
563                 print 'Series directory %s is not empty.' % self.__name
564             if not os.listdir(self.__refs_dir):
565                 os.rmdir(self.__refs_dir)
566             else:
567                 print 'Refs directory %s is not empty.' % self.__refs_dir
568
569         if os.path.exists(self.__base_file):
570             os.remove(self.__base_file)
571
572     def refresh_patch(self, files = None, message = None, edit = False,
573                       show_patch = False,
574                       cache_update = True,
575                       author_name = None, author_email = None,
576                       author_date = None,
577                       committer_name = None, committer_email = None,
578                       backup = False):
579         """Generates a new commit for the given patch
580         """
581         name = self.get_current()
582         if not name:
583             raise StackException, 'No patches applied'
584
585         patch = Patch(name, self.__patch_dir, self.__refs_dir)
586
587         descr = patch.get_description()
588         if not (message or descr):
589             edit = True
590             descr = ''
591         elif message:
592             descr = message
593
594         if not message and edit:
595             descr = edit_file(self, descr.rstrip(), \
596                               'Please edit the description for patch "%s" ' \
597                               'above.' % name, show_patch)
598
599         if not author_name:
600             author_name = patch.get_authname()
601         if not author_email:
602             author_email = patch.get_authemail()
603         if not author_date:
604             author_date = patch.get_authdate()
605         if not committer_name:
606             committer_name = patch.get_commname()
607         if not committer_email:
608             committer_email = patch.get_commemail()
609
610         bottom = patch.get_bottom()
611
612         commit_id = git.commit(files = files,
613                                message = descr, parents = [bottom],
614                                cache_update = cache_update,
615                                allowempty = True,
616                                author_name = author_name,
617                                author_email = author_email,
618                                author_date = author_date,
619                                committer_name = committer_name,
620                                committer_email = committer_email)
621
622         patch.set_bottom(bottom, backup = backup)
623         patch.set_top(commit_id, backup = backup)
624         patch.set_description(descr)
625         patch.set_authname(author_name)
626         patch.set_authemail(author_email)
627         patch.set_authdate(author_date)
628         patch.set_commname(committer_name)
629         patch.set_commemail(committer_email)
630
631         return commit_id
632
633     def undo_refresh(self):
634         """Undo the patch boundaries changes caused by 'refresh'
635         """
636         name = self.get_current()
637         assert(name)
638
639         patch = Patch(name, self.__patch_dir, self.__refs_dir)
640         old_bottom = patch.get_old_bottom()
641         old_top = patch.get_old_top()
642
643         # the bottom of the patch is not changed by refresh. If the
644         # old_bottom is different, there wasn't any previous 'refresh'
645         # command (probably only a 'push')
646         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
647             raise StackException, 'No refresh undo information available'
648
649         git.reset(tree_id = old_top, check_out = False)
650         patch.restore_old_boundaries()
651
652     def new_patch(self, name, message = None, can_edit = True,
653                   unapplied = False, show_patch = False,
654                   top = None, bottom = None,
655                   author_name = None, author_email = None, author_date = None,
656                   committer_name = None, committer_email = None,
657                   before_existing = False):
658         """Creates a new patch
659         """
660         if self.__patch_applied(name) or self.__patch_unapplied(name):
661             raise StackException, 'Patch "%s" already exists' % name
662
663         if not message and can_edit:
664             descr = edit_file(self, None, \
665                               'Please enter the description for patch "%s" ' \
666                               'above.' % name, show_patch)
667         else:
668             descr = message
669
670         head = git.get_head()
671
672         self.__begin_stack_check()
673
674         patch = Patch(name, self.__patch_dir, self.__refs_dir)
675         patch.create()
676
677         if bottom:
678             patch.set_bottom(bottom)
679         else:
680             patch.set_bottom(head)
681         if top:
682             patch.set_top(top)
683         else:
684             patch.set_top(head)
685
686         patch.set_description(descr)
687         patch.set_authname(author_name)
688         patch.set_authemail(author_email)
689         patch.set_authdate(author_date)
690         patch.set_commname(committer_name)
691         patch.set_commemail(committer_email)
692
693         if unapplied:
694             patches = [patch.get_name()] + self.get_unapplied()
695
696             f = file(self.__unapplied_file, 'w+')
697             f.writelines([line + '\n' for line in patches])
698             f.close()
699         else:
700             if before_existing:
701                 insert_string(self.__applied_file, patch.get_name())
702                 if not self.get_current():
703                     self.__set_current(name)
704             else:
705                 append_string(self.__applied_file, patch.get_name())
706                 self.__set_current(name)
707
708     def delete_patch(self, name):
709         """Deletes a patch
710         """
711         patch = Patch(name, self.__patch_dir, self.__refs_dir)
712
713         if self.__patch_is_current(patch):
714             self.pop_patch(name)
715         elif self.__patch_applied(name):
716             raise StackException, 'Cannot remove an applied patch, "%s", ' \
717                   'which is not current' % name
718         elif not name in self.get_unapplied():
719             raise StackException, 'Unknown patch "%s"' % name
720
721         patch.delete()
722
723         unapplied = self.get_unapplied()
724         unapplied.remove(name)
725         f = file(self.__unapplied_file, 'w+')
726         f.writelines([line + '\n' for line in unapplied])
727         f.close()
728         self.__begin_stack_check()
729
730     def forward_patches(self, names):
731         """Try to fast-forward an array of patches.
732
733         On return, patches in names[0:returned_value] have been pushed on the
734         stack. Apply the rest with push_patch
735         """
736         unapplied = self.get_unapplied()
737         self.__begin_stack_check()
738
739         forwarded = 0
740         top = git.get_head()
741
742         for name in names:
743             assert(name in unapplied)
744
745             patch = Patch(name, self.__patch_dir, self.__refs_dir)
746
747             head = top
748             bottom = patch.get_bottom()
749             top = patch.get_top()
750
751             # top != bottom always since we have a commit for each patch
752             if head == bottom:
753                 # reset the backup information
754                 patch.set_bottom(head, backup = True)
755                 patch.set_top(top, backup = True)
756
757             else:
758                 head_tree = git.get_commit(head).get_tree()
759                 bottom_tree = git.get_commit(bottom).get_tree()
760                 if head_tree == bottom_tree:
761                     # We must just reparent this patch and create a new commit
762                     # for it
763                     descr = patch.get_description()
764                     author_name = patch.get_authname()
765                     author_email = patch.get_authemail()
766                     author_date = patch.get_authdate()
767                     committer_name = patch.get_commname()
768                     committer_email = patch.get_commemail()
769
770                     top_tree = git.get_commit(top).get_tree()
771
772                     top = git.commit(message = descr, parents = [head],
773                                      cache_update = False,
774                                      tree_id = top_tree,
775                                      allowempty = True,
776                                      author_name = author_name,
777                                      author_email = author_email,
778                                      author_date = author_date,
779                                      committer_name = committer_name,
780                                      committer_email = committer_email)
781
782                     patch.set_bottom(head, backup = True)
783                     patch.set_top(top, backup = True)
784                 else:
785                     top = head
786                     # stop the fast-forwarding, must do a real merge
787                     break
788
789             forwarded+=1
790             unapplied.remove(name)
791
792         if forwarded == 0:
793             return 0
794
795         git.switch(top)
796
797         append_strings(self.__applied_file, names[0:forwarded])
798
799         f = file(self.__unapplied_file, 'w+')
800         f.writelines([line + '\n' for line in unapplied])
801         f.close()
802
803         self.__set_current(name)
804
805         return forwarded
806
807     def merged_patches(self, names):
808         """Test which patches were merged upstream by reverse-applying
809         them in reverse order. The function returns the list of
810         patches detected to have been applied. The state of the tree
811         is restored to the original one
812         """
813         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
814                    for name in names]
815         patches.reverse()
816
817         merged = []
818         for p in patches:
819             if git.apply_diff(p.get_top(), p.get_bottom(), False):
820                 merged.append(p.get_name())
821         merged.reverse()
822
823         git.reset()
824
825         return merged
826
827     def push_patch(self, name, empty = False):
828         """Pushes a patch on the stack
829         """
830         unapplied = self.get_unapplied()
831         assert(name in unapplied)
832
833         self.__begin_stack_check()
834
835         patch = Patch(name, self.__patch_dir, self.__refs_dir)
836
837         head = git.get_head()
838         bottom = patch.get_bottom()
839         top = patch.get_top()
840
841         ex = None
842         modified = False
843
844         # top != bottom always since we have a commit for each patch
845         if empty:
846             # just make an empty patch (top = bottom = HEAD). This
847             # option is useful to allow undoing already merged
848             # patches. The top is updated by refresh_patch since we
849             # need an empty commit
850             patch.set_bottom(head, backup = True)
851             patch.set_top(head, backup = True)
852             modified = True
853         elif head == bottom:
854             # reset the backup information
855             patch.set_bottom(bottom, backup = True)
856             patch.set_top(top, backup = True)
857
858             git.switch(top)
859         else:
860             # new patch needs to be refreshed.
861             # The current patch is empty after merge.
862             patch.set_bottom(head, backup = True)
863             patch.set_top(head, backup = True)
864
865             # Try the fast applying first. If this fails, fall back to the
866             # three-way merge
867             if not git.apply_diff(bottom, top):
868                 # if git.apply_diff() fails, the patch requires a diff3
869                 # merge and can be reported as modified
870                 modified = True
871
872                 # merge can fail but the patch needs to be pushed
873                 try:
874                     git.merge(bottom, head, top)
875                 except git.GitException, ex:
876                     print >> sys.stderr, \
877                           'The merge failed during "push". ' \
878                           'Use "refresh" after fixing the conflicts'
879
880         append_string(self.__applied_file, name)
881
882         unapplied.remove(name)
883         f = file(self.__unapplied_file, 'w+')
884         f.writelines([line + '\n' for line in unapplied])
885         f.close()
886
887         self.__set_current(name)
888
889         # head == bottom case doesn't need to refresh the patch
890         if empty or head != bottom:
891             if not ex:
892                 # if the merge was OK and no conflicts, just refresh the patch
893                 # The GIT cache was already updated by the merge operation
894                 self.refresh_patch(cache_update = False)
895             else:
896                 raise StackException, str(ex)
897
898         return modified
899
900     def undo_push(self):
901         name = self.get_current()
902         assert(name)
903
904         patch = Patch(name, self.__patch_dir, self.__refs_dir)
905         old_bottom = patch.get_old_bottom()
906         old_top = patch.get_old_top()
907
908         # the top of the patch is changed by a push operation only
909         # together with the bottom (otherwise the top was probably
910         # modified by 'refresh'). If they are both unchanged, there
911         # was a fast forward
912         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
913             raise StackException, 'No push undo information available'
914
915         git.reset()
916         self.pop_patch(name)
917         return patch.restore_old_boundaries()
918
919     def pop_patch(self, name):
920         """Pops the top patch from the stack
921         """
922         applied = self.get_applied()
923         applied.reverse()
924         assert(name in applied)
925
926         patch = Patch(name, self.__patch_dir, self.__refs_dir)
927
928         git.switch(patch.get_bottom())
929
930         # save the new applied list
931         idx = applied.index(name) + 1
932
933         popped = applied[:idx]
934         popped.reverse()
935         unapplied = popped + self.get_unapplied()
936
937         f = file(self.__unapplied_file, 'w+')
938         f.writelines([line + '\n' for line in unapplied])
939         f.close()
940
941         del applied[:idx]
942         applied.reverse()
943
944         f = file(self.__applied_file, 'w+')
945         f.writelines([line + '\n' for line in applied])
946         f.close()
947
948         if applied == []:
949             self.__set_current(None)
950         else:
951             self.__set_current(applied[-1])
952
953         self.__end_stack_check()
954
955     def empty_patch(self, name):
956         """Returns True if the patch is empty
957         """
958         patch = Patch(name, self.__patch_dir, self.__refs_dir)
959         bottom = patch.get_bottom()
960         top = patch.get_top()
961
962         if bottom == top:
963             return True
964         elif git.get_commit(top).get_tree() \
965                  == git.get_commit(bottom).get_tree():
966             return True
967
968         return False
969
970     def rename_patch(self, oldname, newname):
971         applied = self.get_applied()
972         unapplied = self.get_unapplied()
973
974         if oldname == newname:
975             raise StackException, '"To" name and "from" name are the same'
976
977         if newname in applied or newname in unapplied:
978             raise StackException, 'Patch "%s" already exists' % newname
979
980         if oldname in unapplied:
981             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
982             unapplied[unapplied.index(oldname)] = newname
983
984             f = file(self.__unapplied_file, 'w+')
985             f.writelines([line + '\n' for line in unapplied])
986             f.close()
987         elif oldname in applied:
988             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
989             if oldname == self.get_current():
990                 self.__set_current(newname)
991
992             applied[applied.index(oldname)] = newname
993
994             f = file(self.__applied_file, 'w+')
995             f.writelines([line + '\n' for line in applied])
996             f.close()
997         else:
998             raise StackException, 'Unknown patch "%s"' % oldname