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