chiark / gitweb /
e50f189b9bd3aad0d591b97ca3b290e7e9a40f38
[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, templates
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 = '.stgitmsg.txt'
69     tmpl = templates.get_template('patchdescr.tmpl')
70
71     f = file(fname, 'w+')
72     if line:
73         print >> f, line
74     elif tmpl:
75         print >> f, tmpl,
76     else:
77         print >> f
78     print >> f, __comment_prefix, comment
79     print >> f, __comment_prefix, \
80           'Lines prefixed with "%s" will be automatically removed.' \
81           % __comment_prefix
82     print >> f, __comment_prefix, \
83           'Trailing empty lines will be automatically removed.'
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         self.__log_ref_file = os.path.join(self.__refs_dir,
132                                            self.__name + '.log')
133
134     def create(self):
135         os.mkdir(self.__dir)
136         create_empty_file(os.path.join(self.__dir, 'bottom'))
137         create_empty_file(os.path.join(self.__dir, 'top'))
138
139     def delete(self):
140         for f in os.listdir(self.__dir):
141             os.remove(os.path.join(self.__dir, f))
142         os.rmdir(self.__dir)
143         os.remove(self.__top_ref_file)
144         if os.path.exists(self.__log_ref_file):
145             os.remove(self.__log_ref_file)
146
147     def get_name(self):
148         return self.__name
149
150     def rename(self, newname):
151         olddir = self.__dir
152         old_top_ref_file = self.__top_ref_file
153         old_log_ref_file = self.__log_ref_file
154         self.__name = newname
155         self.__dir = os.path.join(self.__series_dir, self.__name)
156         self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
157         self.__log_ref_file = os.path.join(self.__refs_dir,
158                                            self.__name + '.log')
159
160         os.rename(olddir, self.__dir)
161         os.rename(old_top_ref_file, self.__top_ref_file)
162         if os.path.exists(old_log_ref_file):
163             os.rename(old_log_ref_file, self.__log_ref_file)
164
165     def __update_top_ref(self, ref):
166         write_string(self.__top_ref_file, ref)
167
168     def __update_log_ref(self, ref):
169         write_string(self.__log_ref_file, ref)
170
171     def update_top_ref(self):
172         top = self.get_top()
173         if top:
174             self.__update_top_ref(top)
175
176     def __get_field(self, name, multiline = False):
177         id_file = os.path.join(self.__dir, name)
178         if os.path.isfile(id_file):
179             line = read_string(id_file, multiline)
180             if line == '':
181                 return None
182             else:
183                 return line
184         else:
185             return None
186
187     def __set_field(self, name, value, multiline = False):
188         fname = os.path.join(self.__dir, name)
189         if value and value != '':
190             write_string(fname, value, multiline)
191         elif os.path.isfile(fname):
192             os.remove(fname)
193
194     def get_old_bottom(self):
195         return self.__get_field('bottom.old')
196
197     def get_bottom(self):
198         return self.__get_field('bottom')
199
200     def set_bottom(self, value, backup = False):
201         if backup:
202             curr = self.__get_field('bottom')
203             self.__set_field('bottom.old', curr)
204         self.__set_field('bottom', value)
205
206     def get_old_top(self):
207         return self.__get_field('top.old')
208
209     def get_top(self):
210         return self.__get_field('top')
211
212     def set_top(self, value, backup = False):
213         if backup:
214             curr = self.__get_field('top')
215             self.__set_field('top.old', curr)
216         self.__set_field('top', value)
217         self.__update_top_ref(value)
218
219     def restore_old_boundaries(self):
220         bottom = self.__get_field('bottom.old')
221         top = self.__get_field('top.old')
222
223         if top and bottom:
224             self.__set_field('bottom', bottom)
225             self.__set_field('top', top)
226             self.__update_top_ref(top)
227             return True
228         else:
229             return False
230
231     def get_description(self):
232         return self.__get_field('description', True)
233
234     def set_description(self, line):
235         self.__set_field('description', line, True)
236
237     def get_authname(self):
238         return self.__get_field('authname')
239
240     def set_authname(self, name):
241         if not name:
242             if config.has_option('stgit', 'authname'):
243                 name = config.get('stgit', 'authname')
244             elif 'GIT_AUTHOR_NAME' in os.environ:
245                 name = os.environ['GIT_AUTHOR_NAME']
246         self.__set_field('authname', name)
247
248     def get_authemail(self):
249         return self.__get_field('authemail')
250
251     def set_authemail(self, address):
252         if not address:
253             if config.has_option('stgit', 'authemail'):
254                 address = config.get('stgit', 'authemail')
255             elif 'GIT_AUTHOR_EMAIL' in os.environ:
256                 address = os.environ['GIT_AUTHOR_EMAIL']
257         self.__set_field('authemail', address)
258
259     def get_authdate(self):
260         return self.__get_field('authdate')
261
262     def set_authdate(self, date):
263         if not date and 'GIT_AUTHOR_DATE' in os.environ:
264             date = os.environ['GIT_AUTHOR_DATE']
265         self.__set_field('authdate', date)
266
267     def get_commname(self):
268         return self.__get_field('commname')
269
270     def set_commname(self, name):
271         if not name:
272             if config.has_option('stgit', 'commname'):
273                 name = config.get('stgit', 'commname')
274             elif 'GIT_COMMITTER_NAME' in os.environ:
275                 name = os.environ['GIT_COMMITTER_NAME']
276         self.__set_field('commname', name)
277
278     def get_commemail(self):
279         return self.__get_field('commemail')
280
281     def set_commemail(self, address):
282         if not address:
283             if config.has_option('stgit', 'commemail'):
284                 address = config.get('stgit', 'commemail')
285             elif 'GIT_COMMITTER_EMAIL' in os.environ:
286                 address = os.environ['GIT_COMMITTER_EMAIL']
287         self.__set_field('commemail', address)
288
289     def get_log(self):
290         return self.__get_field('log')
291
292     def set_log(self, value, backup = False):
293         self.__set_field('log', value)
294         self.__update_log_ref(value)
295
296
297 class Series:
298     """Class including the operations on series
299     """
300     def __init__(self, name = None):
301         """Takes a series name as the parameter.
302         """
303         try:
304             if name:
305                 self.__name = name
306             else:
307                 self.__name = git.get_head_file()
308             self.__base_dir = basedir.get()
309         except git.GitException, ex:
310             raise StackException, 'GIT tree not initialised: %s' % ex
311
312         self.__series_dir = os.path.join(self.__base_dir, 'patches',
313                                          self.__name)
314         self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
315                                        self.__name)
316         self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
317                                         self.__name)
318
319         self.__applied_file = os.path.join(self.__series_dir, 'applied')
320         self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied')
321         self.__current_file = os.path.join(self.__series_dir, 'current')
322         self.__descr_file = os.path.join(self.__series_dir, 'description')
323
324         # where this series keeps its patches
325         self.__patch_dir = os.path.join(self.__series_dir, 'patches')
326         if not os.path.isdir(self.__patch_dir):
327             self.__patch_dir = self.__series_dir
328
329         # if no __refs_dir, create and populate it (upgrade old repositories)
330         if self.is_initialised() and not os.path.isdir(self.__refs_dir):
331             os.makedirs(self.__refs_dir)
332             for patch in self.get_applied() + self.get_unapplied():
333                 self.get_patch(patch).update_top_ref()
334
335     def get_branch(self):
336         """Return the branch name for the Series object
337         """
338         return self.__name
339
340     def __set_current(self, name):
341         """Sets the topmost patch
342         """
343         if name:
344             write_string(self.__current_file, name)
345         else:
346             create_empty_file(self.__current_file)
347
348     def get_patch(self, name):
349         """Return a Patch object for the given name
350         """
351         return Patch(name, self.__patch_dir, self.__refs_dir)
352
353     def get_current_patch(self):
354         """Return a Patch object representing the topmost patch, or
355         None if there is no such patch."""
356         crt = self.get_current()
357         if not crt:
358             return None
359         return Patch(crt, self.__patch_dir, self.__refs_dir)
360
361     def get_current(self):
362         """Return the name of the topmost patch, or None if there is
363         no such patch."""
364         if os.path.isfile(self.__current_file):
365             name = read_string(self.__current_file)
366         else:
367             return None
368         if name == '':
369             return None
370         else:
371             return name
372
373     def get_applied(self):
374         if not os.path.isfile(self.__applied_file):
375             raise StackException, 'Branch "%s" not initialised' % self.__name
376         f = file(self.__applied_file)
377         names = [line.strip() for line in f.readlines()]
378         f.close()
379         return names
380
381     def get_unapplied(self):
382         if not os.path.isfile(self.__unapplied_file):
383             raise StackException, 'Branch "%s" not initialised' % self.__name
384         f = file(self.__unapplied_file)
385         names = [line.strip() for line in f.readlines()]
386         f.close()
387         return names
388
389     def get_base_file(self):
390         self.__begin_stack_check()
391         return self.__base_file
392
393     def get_protected(self):
394         return os.path.isfile(os.path.join(self.__series_dir, 'protected'))
395
396     def protect(self):
397         protect_file = os.path.join(self.__series_dir, 'protected')
398         if not os.path.isfile(protect_file):
399             create_empty_file(protect_file)
400
401     def unprotect(self):
402         protect_file = os.path.join(self.__series_dir, 'protected')
403         if os.path.isfile(protect_file):
404             os.remove(protect_file)
405
406     def get_description(self):
407         if os.path.isfile(self.__descr_file):
408             return read_string(self.__descr_file)
409         else:
410             return ''
411
412     def __patch_is_current(self, patch):
413         return patch.get_name() == read_string(self.__current_file)
414
415     def __patch_applied(self, name):
416         """Return true if the patch exists in the applied list
417         """
418         return name in self.get_applied()
419
420     def __patch_unapplied(self, name):
421         """Return true if the patch exists in the unapplied list
422         """
423         return name in self.get_unapplied()
424
425     def patch_exists(self, name):
426         """Return true if there is a patch with the given name, false
427         otherwise."""
428         return self.__patch_applied(name) or self.__patch_applied(name)
429
430     def __begin_stack_check(self):
431         """Save the current HEAD into .git/refs/heads/base if the stack
432         is empty
433         """
434         if len(self.get_applied()) == 0:
435             head = git.get_head()
436             write_string(self.__base_file, head)
437
438     def __end_stack_check(self):
439         """Remove .git/refs/heads/base if the stack is empty.
440         This warning should never happen
441         """
442         if len(self.get_applied()) == 0 \
443            and read_string(self.__base_file) != git.get_head():
444             print 'Warning: stack empty but the HEAD and base are different'
445
446     def head_top_equal(self):
447         """Return true if the head and the top are the same
448         """
449         crt = self.get_current_patch()
450         if not crt:
451             # we don't care, no patches applied
452             return True
453         return git.get_head() == crt.get_top()
454
455     def is_initialised(self):
456         """Checks if series is already initialised
457         """
458         return os.path.isdir(self.__patch_dir)
459
460     def init(self, create_at=False):
461         """Initialises the stgit series
462         """
463         bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
464
465         if os.path.exists(self.__patch_dir):
466             raise StackException, self.__patch_dir + ' already exists'
467         if os.path.exists(self.__refs_dir):
468             raise StackException, self.__refs_dir + ' already exists'
469         if os.path.exists(self.__base_file):
470             raise StackException, self.__base_file + ' already exists'
471
472         if (create_at!=False):
473             git.create_branch(self.__name, create_at)
474
475         os.makedirs(self.__patch_dir)
476
477         create_dirs(bases_dir)
478
479         create_empty_file(self.__applied_file)
480         create_empty_file(self.__unapplied_file)
481         create_empty_file(self.__descr_file)
482         os.makedirs(os.path.join(self.__series_dir, 'patches'))
483         os.makedirs(self.__refs_dir)
484         self.__begin_stack_check()
485
486     def convert(self):
487         """Either convert to use a separate patch directory, or
488         unconvert to place the patches in the same directory with
489         series control files
490         """
491         if self.__patch_dir == self.__series_dir:
492             print 'Converting old-style to new-style...',
493             sys.stdout.flush()
494
495             self.__patch_dir = os.path.join(self.__series_dir, 'patches')
496             os.makedirs(self.__patch_dir)
497
498             for p in self.get_applied() + self.get_unapplied():
499                 src = os.path.join(self.__series_dir, p)
500                 dest = os.path.join(self.__patch_dir, p)
501                 os.rename(src, dest)
502
503             print 'done'
504
505         else:
506             print 'Converting new-style to old-style...',
507             sys.stdout.flush()
508
509             for p in self.get_applied() + self.get_unapplied():
510                 src = os.path.join(self.__patch_dir, p)
511                 dest = os.path.join(self.__series_dir, p)
512                 os.rename(src, dest)
513
514             if not os.listdir(self.__patch_dir):
515                 os.rmdir(self.__patch_dir)
516                 print 'done'
517             else:
518                 print 'Patch directory %s is not empty.' % self.__name
519
520             self.__patch_dir = self.__series_dir
521
522     def rename(self, to_name):
523         """Renames a series
524         """
525         to_stack = Series(to_name)
526
527         if to_stack.is_initialised():
528             raise StackException, '"%s" already exists' % to_stack.get_branch()
529         if os.path.exists(to_stack.__base_file):
530             os.remove(to_stack.__base_file)
531
532         git.rename_branch(self.__name, to_name)
533
534         if os.path.isdir(self.__series_dir):
535             rename(os.path.join(self.__base_dir, 'patches'),
536                    self.__name, to_stack.__name)
537         if os.path.exists(self.__base_file):
538             rename(os.path.join(self.__base_dir, 'refs', 'bases'),
539                    self.__name, to_stack.__name)
540         if os.path.exists(self.__refs_dir):
541             rename(os.path.join(self.__base_dir, 'refs', 'patches'),
542                    self.__name, to_stack.__name)
543
544         self.__init__(to_name)
545
546     def clone(self, target_series):
547         """Clones a series
548         """
549         base = read_string(self.get_base_file())
550         Series(target_series).init(create_at = base)
551         new_series = Series(target_series)
552
553         # generate an artificial description file
554         write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
555
556         # clone self's entire series as unapplied patches
557         patches = self.get_applied() + self.get_unapplied()
558         patches.reverse()
559         for p in patches:
560             patch = self.get_patch(p)
561             new_series.new_patch(p, message = patch.get_description(),
562                                  can_edit = False, unapplied = True,
563                                  bottom = patch.get_bottom(),
564                                  top = patch.get_top(),
565                                  author_name = patch.get_authname(),
566                                  author_email = patch.get_authemail(),
567                                  author_date = patch.get_authdate())
568
569         # fast forward the cloned series to self's top
570         new_series.forward_patches(self.get_applied())
571
572     def delete(self, force = False):
573         """Deletes an stgit series
574         """
575         if self.is_initialised():
576             patches = self.get_unapplied() + self.get_applied()
577             if not force and patches:
578                 raise StackException, \
579                       'Cannot delete: the series still contains patches'
580             for p in patches:
581                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
582
583             if os.path.exists(self.__applied_file):
584                 os.remove(self.__applied_file)
585             if os.path.exists(self.__unapplied_file):
586                 os.remove(self.__unapplied_file)
587             if os.path.exists(self.__current_file):
588                 os.remove(self.__current_file)
589             if os.path.exists(self.__descr_file):
590                 os.remove(self.__descr_file)
591             if not os.listdir(self.__patch_dir):
592                 os.rmdir(self.__patch_dir)
593             else:
594                 print 'Patch directory %s is not empty.' % self.__name
595             if not os.listdir(self.__series_dir):
596                 remove_dirs(os.path.join(self.__base_dir, 'patches'),
597                             self.__name)
598             else:
599                 print 'Series directory %s is not empty.' % self.__name
600             if not os.listdir(self.__refs_dir):
601                 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
602                             self.__name)
603             else:
604                 print 'Refs directory %s is not empty.' % self.__refs_dir
605
606         if os.path.exists(self.__base_file):
607             remove_file_and_dirs(
608                 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
609
610     def refresh_patch(self, files = None, message = None, edit = False,
611                       show_patch = False,
612                       cache_update = True,
613                       author_name = None, author_email = None,
614                       author_date = None,
615                       committer_name = None, committer_email = None,
616                       backup = False, sign_str = None, log = 'refresh'):
617         """Generates a new commit for the given patch
618         """
619         name = self.get_current()
620         if not name:
621             raise StackException, 'No patches applied'
622
623         patch = Patch(name, self.__patch_dir, self.__refs_dir)
624
625         descr = patch.get_description()
626         if not (message or descr):
627             edit = True
628             descr = ''
629         elif message:
630             descr = message
631
632         if not message and edit:
633             descr = edit_file(self, descr.rstrip(), \
634                               'Please edit the description for patch "%s" ' \
635                               'above.' % name, show_patch)
636
637         if not author_name:
638             author_name = patch.get_authname()
639         if not author_email:
640             author_email = patch.get_authemail()
641         if not author_date:
642             author_date = patch.get_authdate()
643         if not committer_name:
644             committer_name = patch.get_commname()
645         if not committer_email:
646             committer_email = patch.get_commemail()
647
648         if sign_str:
649             descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
650                                            committer_name, committer_email)
651
652         bottom = patch.get_bottom()
653
654         commit_id = git.commit(files = files,
655                                message = descr, parents = [bottom],
656                                cache_update = cache_update,
657                                allowempty = True,
658                                author_name = author_name,
659                                author_email = author_email,
660                                author_date = author_date,
661                                committer_name = committer_name,
662                                committer_email = committer_email)
663
664         patch.set_bottom(bottom, backup = backup)
665         patch.set_top(commit_id, backup = backup)
666         patch.set_description(descr)
667         patch.set_authname(author_name)
668         patch.set_authemail(author_email)
669         patch.set_authdate(author_date)
670         patch.set_commname(committer_name)
671         patch.set_commemail(committer_email)
672
673         if log:
674             self.log_patch(patch, log)
675
676         return commit_id
677
678     def undo_refresh(self):
679         """Undo the patch boundaries changes caused by 'refresh'
680         """
681         name = self.get_current()
682         assert(name)
683
684         patch = Patch(name, self.__patch_dir, self.__refs_dir)
685         old_bottom = patch.get_old_bottom()
686         old_top = patch.get_old_top()
687
688         # the bottom of the patch is not changed by refresh. If the
689         # old_bottom is different, there wasn't any previous 'refresh'
690         # command (probably only a 'push')
691         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
692             raise StackException, 'No refresh undo information available'
693
694         git.reset(tree_id = old_top, check_out = False)
695         if patch.restore_old_boundaries():
696             self.log_patch(patch, 'undo')
697
698     def new_patch(self, name, message = None, can_edit = True,
699                   unapplied = False, show_patch = False,
700                   top = None, bottom = None,
701                   author_name = None, author_email = None, author_date = None,
702                   committer_name = None, committer_email = None,
703                   before_existing = False, refresh = True):
704         """Creates a new patch
705         """
706         if self.__patch_applied(name) or self.__patch_unapplied(name):
707             raise StackException, 'Patch "%s" already exists' % name
708
709         if not message and can_edit:
710             descr = edit_file(self, None, \
711                               'Please enter the description for patch "%s" ' \
712                               'above.' % name, show_patch)
713         else:
714             descr = message
715
716         head = git.get_head()
717
718         self.__begin_stack_check()
719
720         patch = Patch(name, self.__patch_dir, self.__refs_dir)
721         patch.create()
722
723         if bottom:
724             patch.set_bottom(bottom)
725         else:
726             patch.set_bottom(head)
727         if top:
728             patch.set_top(top)
729         else:
730             patch.set_top(head)
731
732         patch.set_description(descr)
733         patch.set_authname(author_name)
734         patch.set_authemail(author_email)
735         patch.set_authdate(author_date)
736         patch.set_commname(committer_name)
737         patch.set_commemail(committer_email)
738
739         if unapplied:
740             self.log_patch(patch, 'new')
741
742             patches = [patch.get_name()] + self.get_unapplied()
743
744             f = file(self.__unapplied_file, 'w+')
745             f.writelines([line + '\n' for line in patches])
746             f.close()
747         elif before_existing:
748             self.log_patch(patch, 'new')
749
750             insert_string(self.__applied_file, patch.get_name())
751             if not self.get_current():
752                 self.__set_current(name)
753         else:
754             append_string(self.__applied_file, patch.get_name())
755             self.__set_current(name)
756             if refresh:
757                 self.refresh_patch(cache_update = False, log = 'new')
758
759     def delete_patch(self, name):
760         """Deletes a patch
761         """
762         patch = Patch(name, self.__patch_dir, self.__refs_dir)
763
764         if self.__patch_is_current(patch):
765             self.pop_patch(name)
766         elif self.__patch_applied(name):
767             raise StackException, 'Cannot remove an applied patch, "%s", ' \
768                   'which is not current' % name
769         elif not name in self.get_unapplied():
770             raise StackException, 'Unknown patch "%s"' % name
771
772         patch.delete()
773
774         unapplied = self.get_unapplied()
775         unapplied.remove(name)
776         f = file(self.__unapplied_file, 'w+')
777         f.writelines([line + '\n' for line in unapplied])
778         f.close()
779         self.__begin_stack_check()
780
781     def forward_patches(self, names):
782         """Try to fast-forward an array of patches.
783
784         On return, patches in names[0:returned_value] have been pushed on the
785         stack. Apply the rest with push_patch
786         """
787         unapplied = self.get_unapplied()
788         self.__begin_stack_check()
789
790         forwarded = 0
791         top = git.get_head()
792
793         for name in names:
794             assert(name in unapplied)
795
796             patch = Patch(name, self.__patch_dir, self.__refs_dir)
797
798             head = top
799             bottom = patch.get_bottom()
800             top = patch.get_top()
801
802             # top != bottom always since we have a commit for each patch
803             if head == bottom:
804                 # reset the backup information. No logging since the
805                 # patch hasn't changed
806                 patch.set_bottom(head, backup = True)
807                 patch.set_top(top, backup = True)
808
809             else:
810                 head_tree = git.get_commit(head).get_tree()
811                 bottom_tree = git.get_commit(bottom).get_tree()
812                 if head_tree == bottom_tree:
813                     # We must just reparent this patch and create a new commit
814                     # for it
815                     descr = patch.get_description()
816                     author_name = patch.get_authname()
817                     author_email = patch.get_authemail()
818                     author_date = patch.get_authdate()
819                     committer_name = patch.get_commname()
820                     committer_email = patch.get_commemail()
821
822                     top_tree = git.get_commit(top).get_tree()
823
824                     top = git.commit(message = descr, parents = [head],
825                                      cache_update = False,
826                                      tree_id = top_tree,
827                                      allowempty = True,
828                                      author_name = author_name,
829                                      author_email = author_email,
830                                      author_date = author_date,
831                                      committer_name = committer_name,
832                                      committer_email = committer_email)
833
834                     patch.set_bottom(head, backup = True)
835                     patch.set_top(top, backup = True)
836
837                     self.log_patch(patch, 'push(f)')
838                 else:
839                     top = head
840                     # stop the fast-forwarding, must do a real merge
841                     break
842
843             forwarded+=1
844             unapplied.remove(name)
845
846         if forwarded == 0:
847             return 0
848
849         git.switch(top)
850
851         append_strings(self.__applied_file, names[0:forwarded])
852
853         f = file(self.__unapplied_file, 'w+')
854         f.writelines([line + '\n' for line in unapplied])
855         f.close()
856
857         self.__set_current(name)
858
859         return forwarded
860
861     def merged_patches(self, names):
862         """Test which patches were merged upstream by reverse-applying
863         them in reverse order. The function returns the list of
864         patches detected to have been applied. The state of the tree
865         is restored to the original one
866         """
867         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
868                    for name in names]
869         patches.reverse()
870
871         merged = []
872         for p in patches:
873             if git.apply_diff(p.get_top(), p.get_bottom()):
874                 merged.append(p.get_name())
875         merged.reverse()
876
877         git.reset()
878
879         return merged
880
881     def push_patch(self, name, empty = False):
882         """Pushes a patch on the stack
883         """
884         unapplied = self.get_unapplied()
885         assert(name in unapplied)
886
887         self.__begin_stack_check()
888
889         patch = Patch(name, self.__patch_dir, self.__refs_dir)
890
891         head = git.get_head()
892         bottom = patch.get_bottom()
893         top = patch.get_top()
894
895         ex = None
896         modified = False
897
898         # top != bottom always since we have a commit for each patch
899         if empty:
900             # just make an empty patch (top = bottom = HEAD). This
901             # option is useful to allow undoing already merged
902             # patches. The top is updated by refresh_patch since we
903             # need an empty commit
904             patch.set_bottom(head, backup = True)
905             patch.set_top(head, backup = True)
906             modified = True
907         elif head == bottom:
908             # reset the backup information. No need for logging
909             patch.set_bottom(bottom, backup = True)
910             patch.set_top(top, backup = True)
911
912             git.switch(top)
913         else:
914             # new patch needs to be refreshed.
915             # The current patch is empty after merge.
916             patch.set_bottom(head, backup = True)
917             patch.set_top(head, backup = True)
918
919             # Try the fast applying first. If this fails, fall back to the
920             # three-way merge
921             if not git.apply_diff(bottom, top):
922                 # if git.apply_diff() fails, the patch requires a diff3
923                 # merge and can be reported as modified
924                 modified = True
925
926                 # merge can fail but the patch needs to be pushed
927                 try:
928                     git.merge(bottom, head, top)
929                 except git.GitException, ex:
930                     print >> sys.stderr, \
931                           'The merge failed during "push". ' \
932                           'Use "refresh" after fixing the conflicts'
933
934         append_string(self.__applied_file, name)
935
936         unapplied.remove(name)
937         f = file(self.__unapplied_file, 'w+')
938         f.writelines([line + '\n' for line in unapplied])
939         f.close()
940
941         self.__set_current(name)
942
943         # head == bottom case doesn't need to refresh the patch
944         if empty or head != bottom:
945             if not ex:
946                 # if the merge was OK and no conflicts, just refresh the patch
947                 # The GIT cache was already updated by the merge operation
948                 if modified:
949                     log = 'push(m)'
950                 else:
951                     log = 'push'
952                 self.refresh_patch(cache_update = False, log = log)
953             else:
954                 raise StackException, str(ex)
955
956         return modified
957
958     def undo_push(self):
959         name = self.get_current()
960         assert(name)
961
962         patch = Patch(name, self.__patch_dir, self.__refs_dir)
963         old_bottom = patch.get_old_bottom()
964         old_top = patch.get_old_top()
965
966         # the top of the patch is changed by a push operation only
967         # together with the bottom (otherwise the top was probably
968         # modified by 'refresh'). If they are both unchanged, there
969         # was a fast forward
970         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
971             raise StackException, 'No push undo information available'
972
973         git.reset()
974         self.pop_patch(name)
975         ret = patch.restore_old_boundaries()
976         if ret:
977             self.log_patch(patch, 'undo')
978
979         return ret
980
981     def pop_patch(self, name, keep = False):
982         """Pops the top patch from the stack
983         """
984         applied = self.get_applied()
985         applied.reverse()
986         assert(name in applied)
987
988         patch = Patch(name, self.__patch_dir, self.__refs_dir)
989
990         # only keep the local changes
991         if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
992             raise StackException, \
993                   'Failed to pop patches while preserving the local changes'
994
995         git.switch(patch.get_bottom(), keep)
996
997         # save the new applied list
998         idx = applied.index(name) + 1
999
1000         popped = applied[:idx]
1001         popped.reverse()
1002         unapplied = popped + self.get_unapplied()
1003
1004         f = file(self.__unapplied_file, 'w+')
1005         f.writelines([line + '\n' for line in unapplied])
1006         f.close()
1007
1008         del applied[:idx]
1009         applied.reverse()
1010
1011         f = file(self.__applied_file, 'w+')
1012         f.writelines([line + '\n' for line in applied])
1013         f.close()
1014
1015         if applied == []:
1016             self.__set_current(None)
1017         else:
1018             self.__set_current(applied[-1])
1019
1020         self.__end_stack_check()
1021
1022     def empty_patch(self, name):
1023         """Returns True if the patch is empty
1024         """
1025         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1026         bottom = patch.get_bottom()
1027         top = patch.get_top()
1028
1029         if bottom == top:
1030             return True
1031         elif git.get_commit(top).get_tree() \
1032                  == git.get_commit(bottom).get_tree():
1033             return True
1034
1035         return False
1036
1037     def rename_patch(self, oldname, newname):
1038         applied = self.get_applied()
1039         unapplied = self.get_unapplied()
1040
1041         if oldname == newname:
1042             raise StackException, '"To" name and "from" name are the same'
1043
1044         if newname in applied or newname in unapplied:
1045             raise StackException, 'Patch "%s" already exists' % newname
1046
1047         if oldname in unapplied:
1048             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1049             unapplied[unapplied.index(oldname)] = newname
1050
1051             f = file(self.__unapplied_file, 'w+')
1052             f.writelines([line + '\n' for line in unapplied])
1053             f.close()
1054         elif oldname in applied:
1055             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1056             if oldname == self.get_current():
1057                 self.__set_current(newname)
1058
1059             applied[applied.index(oldname)] = newname
1060
1061             f = file(self.__applied_file, 'w+')
1062             f.writelines([line + '\n' for line in applied])
1063             f.close()
1064         else:
1065             raise StackException, 'Unknown patch "%s"' % oldname
1066
1067     def log_patch(self, patch, message):
1068         """Generate a log commit for a patch
1069         """
1070         top = git.get_commit(patch.get_top())
1071         msg = '%s\t%s' % (message, top.get_id_hash())
1072
1073         old_log = patch.get_log()
1074         if old_log:
1075             parents = [old_log]
1076         else:
1077             parents = []
1078
1079         log = git.commit(message = msg, parents = parents,
1080                          cache_update = False, tree_id = top.get_tree(),
1081                          allowempty = True)
1082         patch.set_log(log)