chiark / gitweb /
26a2fc5beda194b6bdb672df30e561808da28f98
[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(self):
354         """Return a Patch object representing the topmost patch
355         """
356         if os.path.isfile(self.__current_file):
357             name = read_string(self.__current_file)
358         else:
359             return None
360         if name == '':
361             return None
362         else:
363             return name
364
365     def get_applied(self):
366         if not os.path.isfile(self.__applied_file):
367             raise StackException, 'Branch "%s" not initialised' % self.__name
368         f = file(self.__applied_file)
369         names = [line.strip() for line in f.readlines()]
370         f.close()
371         return names
372
373     def get_unapplied(self):
374         if not os.path.isfile(self.__unapplied_file):
375             raise StackException, 'Branch "%s" not initialised' % self.__name
376         f = file(self.__unapplied_file)
377         names = [line.strip() for line in f.readlines()]
378         f.close()
379         return names
380
381     def get_base_file(self):
382         self.__begin_stack_check()
383         return self.__base_file
384
385     def get_protected(self):
386         return os.path.isfile(os.path.join(self.__series_dir, 'protected'))
387
388     def protect(self):
389         protect_file = os.path.join(self.__series_dir, 'protected')
390         if not os.path.isfile(protect_file):
391             create_empty_file(protect_file)
392
393     def unprotect(self):
394         protect_file = os.path.join(self.__series_dir, 'protected')
395         if os.path.isfile(protect_file):
396             os.remove(protect_file)
397
398     def get_description(self):
399         if os.path.isfile(self.__descr_file):
400             return read_string(self.__descr_file)
401         else:
402             return ''
403
404     def __patch_is_current(self, patch):
405         return patch.get_name() == read_string(self.__current_file)
406
407     def __patch_applied(self, name):
408         """Return true if the patch exists in the applied list
409         """
410         return name in self.get_applied()
411
412     def __patch_unapplied(self, name):
413         """Return true if the patch exists in the unapplied list
414         """
415         return name in self.get_unapplied()
416
417     def __begin_stack_check(self):
418         """Save the current HEAD into .git/refs/heads/base if the stack
419         is empty
420         """
421         if len(self.get_applied()) == 0:
422             head = git.get_head()
423             write_string(self.__base_file, head)
424
425     def __end_stack_check(self):
426         """Remove .git/refs/heads/base if the stack is empty.
427         This warning should never happen
428         """
429         if len(self.get_applied()) == 0 \
430            and read_string(self.__base_file) != git.get_head():
431             print 'Warning: stack empty but the HEAD and base are different'
432
433     def head_top_equal(self):
434         """Return true if the head and the top are the same
435         """
436         crt = self.get_current()
437         if not crt:
438             # we don't care, no patches applied
439             return True
440         return git.get_head() == Patch(crt, self.__patch_dir,
441                                        self.__refs_dir).get_top()
442
443     def is_initialised(self):
444         """Checks if series is already initialised
445         """
446         return os.path.isdir(self.__patch_dir)
447
448     def init(self, create_at=False):
449         """Initialises the stgit series
450         """
451         bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
452
453         if os.path.exists(self.__patch_dir):
454             raise StackException, self.__patch_dir + ' already exists'
455         if os.path.exists(self.__refs_dir):
456             raise StackException, self.__refs_dir + ' already exists'
457         if os.path.exists(self.__base_file):
458             raise StackException, self.__base_file + ' already exists'
459
460         if (create_at!=False):
461             git.create_branch(self.__name, create_at)
462
463         os.makedirs(self.__patch_dir)
464
465         create_dirs(bases_dir)
466
467         create_empty_file(self.__applied_file)
468         create_empty_file(self.__unapplied_file)
469         create_empty_file(self.__descr_file)
470         os.makedirs(os.path.join(self.__series_dir, 'patches'))
471         os.makedirs(self.__refs_dir)
472         self.__begin_stack_check()
473
474     def convert(self):
475         """Either convert to use a separate patch directory, or
476         unconvert to place the patches in the same directory with
477         series control files
478         """
479         if self.__patch_dir == self.__series_dir:
480             print 'Converting old-style to new-style...',
481             sys.stdout.flush()
482
483             self.__patch_dir = os.path.join(self.__series_dir, 'patches')
484             os.makedirs(self.__patch_dir)
485
486             for p in self.get_applied() + self.get_unapplied():
487                 src = os.path.join(self.__series_dir, p)
488                 dest = os.path.join(self.__patch_dir, p)
489                 os.rename(src, dest)
490
491             print 'done'
492
493         else:
494             print 'Converting new-style to old-style...',
495             sys.stdout.flush()
496
497             for p in self.get_applied() + self.get_unapplied():
498                 src = os.path.join(self.__patch_dir, p)
499                 dest = os.path.join(self.__series_dir, p)
500                 os.rename(src, dest)
501
502             if not os.listdir(self.__patch_dir):
503                 os.rmdir(self.__patch_dir)
504                 print 'done'
505             else:
506                 print 'Patch directory %s is not empty.' % self.__name
507
508             self.__patch_dir = self.__series_dir
509
510     def rename(self, to_name):
511         """Renames a series
512         """
513         to_stack = Series(to_name)
514
515         if to_stack.is_initialised():
516             raise StackException, '"%s" already exists' % to_stack.get_branch()
517         if os.path.exists(to_stack.__base_file):
518             os.remove(to_stack.__base_file)
519
520         git.rename_branch(self.__name, to_name)
521
522         if os.path.isdir(self.__series_dir):
523             rename(os.path.join(self.__base_dir, 'patches'),
524                    self.__name, to_stack.__name)
525         if os.path.exists(self.__base_file):
526             rename(os.path.join(self.__base_dir, 'refs', 'bases'),
527                    self.__name, to_stack.__name)
528         if os.path.exists(self.__refs_dir):
529             rename(os.path.join(self.__base_dir, 'refs', 'patches'),
530                    self.__name, to_stack.__name)
531
532         self.__init__(to_name)
533
534     def clone(self, target_series):
535         """Clones a series
536         """
537         base = read_string(self.get_base_file())
538         Series(target_series).init(create_at = base)
539         new_series = Series(target_series)
540
541         # generate an artificial description file
542         write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
543
544         # clone self's entire series as unapplied patches
545         patches = self.get_applied() + self.get_unapplied()
546         patches.reverse()
547         for p in patches:
548             patch = self.get_patch(p)
549             new_series.new_patch(p, message = patch.get_description(),
550                                  can_edit = False, unapplied = True,
551                                  bottom = patch.get_bottom(),
552                                  top = patch.get_top(),
553                                  author_name = patch.get_authname(),
554                                  author_email = patch.get_authemail(),
555                                  author_date = patch.get_authdate())
556
557         # fast forward the cloned series to self's top
558         new_series.forward_patches(self.get_applied())
559
560     def delete(self, force = False):
561         """Deletes an stgit series
562         """
563         if self.is_initialised():
564             patches = self.get_unapplied() + self.get_applied()
565             if not force and patches:
566                 raise StackException, \
567                       'Cannot delete: the series still contains patches'
568             for p in patches:
569                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
570
571             if os.path.exists(self.__applied_file):
572                 os.remove(self.__applied_file)
573             if os.path.exists(self.__unapplied_file):
574                 os.remove(self.__unapplied_file)
575             if os.path.exists(self.__current_file):
576                 os.remove(self.__current_file)
577             if os.path.exists(self.__descr_file):
578                 os.remove(self.__descr_file)
579             if not os.listdir(self.__patch_dir):
580                 os.rmdir(self.__patch_dir)
581             else:
582                 print 'Patch directory %s is not empty.' % self.__name
583             if not os.listdir(self.__series_dir):
584                 remove_dirs(os.path.join(self.__base_dir, 'patches'),
585                             self.__name)
586             else:
587                 print 'Series directory %s is not empty.' % self.__name
588             if not os.listdir(self.__refs_dir):
589                 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
590                             self.__name)
591             else:
592                 print 'Refs directory %s is not empty.' % self.__refs_dir
593
594         if os.path.exists(self.__base_file):
595             remove_file_and_dirs(
596                 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
597
598     def refresh_patch(self, files = None, message = None, edit = False,
599                       show_patch = False,
600                       cache_update = True,
601                       author_name = None, author_email = None,
602                       author_date = None,
603                       committer_name = None, committer_email = None,
604                       backup = False, sign_str = None, log = 'refresh'):
605         """Generates a new commit for the given patch
606         """
607         name = self.get_current()
608         if not name:
609             raise StackException, 'No patches applied'
610
611         patch = Patch(name, self.__patch_dir, self.__refs_dir)
612
613         descr = patch.get_description()
614         if not (message or descr):
615             edit = True
616             descr = ''
617         elif message:
618             descr = message
619
620         if not message and edit:
621             descr = edit_file(self, descr.rstrip(), \
622                               'Please edit the description for patch "%s" ' \
623                               'above.' % name, show_patch)
624
625         if not author_name:
626             author_name = patch.get_authname()
627         if not author_email:
628             author_email = patch.get_authemail()
629         if not author_date:
630             author_date = patch.get_authdate()
631         if not committer_name:
632             committer_name = patch.get_commname()
633         if not committer_email:
634             committer_email = patch.get_commemail()
635
636         if sign_str:
637             descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
638                                            committer_name, committer_email)
639
640         bottom = patch.get_bottom()
641
642         commit_id = git.commit(files = files,
643                                message = descr, parents = [bottom],
644                                cache_update = cache_update,
645                                allowempty = True,
646                                author_name = author_name,
647                                author_email = author_email,
648                                author_date = author_date,
649                                committer_name = committer_name,
650                                committer_email = committer_email)
651
652         patch.set_bottom(bottom, backup = backup)
653         patch.set_top(commit_id, backup = backup)
654         patch.set_description(descr)
655         patch.set_authname(author_name)
656         patch.set_authemail(author_email)
657         patch.set_authdate(author_date)
658         patch.set_commname(committer_name)
659         patch.set_commemail(committer_email)
660
661         if log:
662             self.log_patch(patch, log)
663
664         return commit_id
665
666     def undo_refresh(self):
667         """Undo the patch boundaries changes caused by 'refresh'
668         """
669         name = self.get_current()
670         assert(name)
671
672         patch = Patch(name, self.__patch_dir, self.__refs_dir)
673         old_bottom = patch.get_old_bottom()
674         old_top = patch.get_old_top()
675
676         # the bottom of the patch is not changed by refresh. If the
677         # old_bottom is different, there wasn't any previous 'refresh'
678         # command (probably only a 'push')
679         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
680             raise StackException, 'No refresh undo information available'
681
682         git.reset(tree_id = old_top, check_out = False)
683         if patch.restore_old_boundaries():
684             self.log_patch(patch, 'undo')
685
686     def new_patch(self, name, message = None, can_edit = True,
687                   unapplied = False, show_patch = False,
688                   top = None, bottom = None,
689                   author_name = None, author_email = None, author_date = None,
690                   committer_name = None, committer_email = None,
691                   before_existing = False):
692         """Creates a new patch
693         """
694         if self.__patch_applied(name) or self.__patch_unapplied(name):
695             raise StackException, 'Patch "%s" already exists' % name
696
697         if not message and can_edit:
698             descr = edit_file(self, None, \
699                               'Please enter the description for patch "%s" ' \
700                               'above.' % name, show_patch)
701         else:
702             descr = message
703
704         head = git.get_head()
705
706         self.__begin_stack_check()
707
708         patch = Patch(name, self.__patch_dir, self.__refs_dir)
709         patch.create()
710
711         if bottom:
712             patch.set_bottom(bottom)
713         else:
714             patch.set_bottom(head)
715         if top:
716             patch.set_top(top)
717         else:
718             patch.set_top(head)
719
720         patch.set_description(descr)
721         patch.set_authname(author_name)
722         patch.set_authemail(author_email)
723         patch.set_authdate(author_date)
724         patch.set_commname(committer_name)
725         patch.set_commemail(committer_email)
726
727         if unapplied:
728             self.log_patch(patch, 'new')
729
730             patches = [patch.get_name()] + self.get_unapplied()
731
732             f = file(self.__unapplied_file, 'w+')
733             f.writelines([line + '\n' for line in patches])
734             f.close()
735         elif before_existing:
736             self.log_patch(patch, 'new')
737
738             insert_string(self.__applied_file, patch.get_name())
739             if not self.get_current():
740                 self.__set_current(name)
741         else:
742             append_string(self.__applied_file, patch.get_name())
743             self.__set_current(name)
744
745             self.refresh_patch(cache_update = False, log = 'new')
746
747     def delete_patch(self, name):
748         """Deletes a patch
749         """
750         patch = Patch(name, self.__patch_dir, self.__refs_dir)
751
752         if self.__patch_is_current(patch):
753             self.pop_patch(name)
754         elif self.__patch_applied(name):
755             raise StackException, 'Cannot remove an applied patch, "%s", ' \
756                   'which is not current' % name
757         elif not name in self.get_unapplied():
758             raise StackException, 'Unknown patch "%s"' % name
759
760         patch.delete()
761
762         unapplied = self.get_unapplied()
763         unapplied.remove(name)
764         f = file(self.__unapplied_file, 'w+')
765         f.writelines([line + '\n' for line in unapplied])
766         f.close()
767         self.__begin_stack_check()
768
769     def forward_patches(self, names):
770         """Try to fast-forward an array of patches.
771
772         On return, patches in names[0:returned_value] have been pushed on the
773         stack. Apply the rest with push_patch
774         """
775         unapplied = self.get_unapplied()
776         self.__begin_stack_check()
777
778         forwarded = 0
779         top = git.get_head()
780
781         for name in names:
782             assert(name in unapplied)
783
784             patch = Patch(name, self.__patch_dir, self.__refs_dir)
785
786             head = top
787             bottom = patch.get_bottom()
788             top = patch.get_top()
789
790             # top != bottom always since we have a commit for each patch
791             if head == bottom:
792                 # reset the backup information. No logging since the
793                 # patch hasn't changed
794                 patch.set_bottom(head, backup = True)
795                 patch.set_top(top, backup = True)
796
797             else:
798                 head_tree = git.get_commit(head).get_tree()
799                 bottom_tree = git.get_commit(bottom).get_tree()
800                 if head_tree == bottom_tree:
801                     # We must just reparent this patch and create a new commit
802                     # for it
803                     descr = patch.get_description()
804                     author_name = patch.get_authname()
805                     author_email = patch.get_authemail()
806                     author_date = patch.get_authdate()
807                     committer_name = patch.get_commname()
808                     committer_email = patch.get_commemail()
809
810                     top_tree = git.get_commit(top).get_tree()
811
812                     top = git.commit(message = descr, parents = [head],
813                                      cache_update = False,
814                                      tree_id = top_tree,
815                                      allowempty = True,
816                                      author_name = author_name,
817                                      author_email = author_email,
818                                      author_date = author_date,
819                                      committer_name = committer_name,
820                                      committer_email = committer_email)
821
822                     patch.set_bottom(head, backup = True)
823                     patch.set_top(top, backup = True)
824
825                     self.log_patch(patch, 'push(f)')
826                 else:
827                     top = head
828                     # stop the fast-forwarding, must do a real merge
829                     break
830
831             forwarded+=1
832             unapplied.remove(name)
833
834         if forwarded == 0:
835             return 0
836
837         git.switch(top)
838
839         append_strings(self.__applied_file, names[0:forwarded])
840
841         f = file(self.__unapplied_file, 'w+')
842         f.writelines([line + '\n' for line in unapplied])
843         f.close()
844
845         self.__set_current(name)
846
847         return forwarded
848
849     def merged_patches(self, names):
850         """Test which patches were merged upstream by reverse-applying
851         them in reverse order. The function returns the list of
852         patches detected to have been applied. The state of the tree
853         is restored to the original one
854         """
855         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
856                    for name in names]
857         patches.reverse()
858
859         merged = []
860         for p in patches:
861             if git.apply_diff(p.get_top(), p.get_bottom()):
862                 merged.append(p.get_name())
863         merged.reverse()
864
865         git.reset()
866
867         return merged
868
869     def push_patch(self, name, empty = False):
870         """Pushes a patch on the stack
871         """
872         unapplied = self.get_unapplied()
873         assert(name in unapplied)
874
875         self.__begin_stack_check()
876
877         patch = Patch(name, self.__patch_dir, self.__refs_dir)
878
879         head = git.get_head()
880         bottom = patch.get_bottom()
881         top = patch.get_top()
882
883         ex = None
884         modified = False
885
886         # top != bottom always since we have a commit for each patch
887         if empty:
888             # just make an empty patch (top = bottom = HEAD). This
889             # option is useful to allow undoing already merged
890             # patches. The top is updated by refresh_patch since we
891             # need an empty commit
892             patch.set_bottom(head, backup = True)
893             patch.set_top(head, backup = True)
894             modified = True
895         elif head == bottom:
896             # reset the backup information. No need for logging
897             patch.set_bottom(bottom, backup = True)
898             patch.set_top(top, backup = True)
899
900             git.switch(top)
901         else:
902             # new patch needs to be refreshed.
903             # The current patch is empty after merge.
904             patch.set_bottom(head, backup = True)
905             patch.set_top(head, backup = True)
906
907             # Try the fast applying first. If this fails, fall back to the
908             # three-way merge
909             if not git.apply_diff(bottom, top):
910                 # if git.apply_diff() fails, the patch requires a diff3
911                 # merge and can be reported as modified
912                 modified = True
913
914                 # merge can fail but the patch needs to be pushed
915                 try:
916                     git.merge(bottom, head, top)
917                 except git.GitException, ex:
918                     print >> sys.stderr, \
919                           'The merge failed during "push". ' \
920                           'Use "refresh" after fixing the conflicts'
921
922         append_string(self.__applied_file, name)
923
924         unapplied.remove(name)
925         f = file(self.__unapplied_file, 'w+')
926         f.writelines([line + '\n' for line in unapplied])
927         f.close()
928
929         self.__set_current(name)
930
931         # head == bottom case doesn't need to refresh the patch
932         if empty or head != bottom:
933             if not ex:
934                 # if the merge was OK and no conflicts, just refresh the patch
935                 # The GIT cache was already updated by the merge operation
936                 if modified:
937                     log = 'push(m)'
938                 else:
939                     log = 'push'
940                 self.refresh_patch(cache_update = False, log = log)
941             else:
942                 raise StackException, str(ex)
943
944         return modified
945
946     def undo_push(self):
947         name = self.get_current()
948         assert(name)
949
950         patch = Patch(name, self.__patch_dir, self.__refs_dir)
951         old_bottom = patch.get_old_bottom()
952         old_top = patch.get_old_top()
953
954         # the top of the patch is changed by a push operation only
955         # together with the bottom (otherwise the top was probably
956         # modified by 'refresh'). If they are both unchanged, there
957         # was a fast forward
958         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
959             raise StackException, 'No push undo information available'
960
961         git.reset()
962         self.pop_patch(name)
963         ret = patch.restore_old_boundaries()
964         if ret:
965             self.log_patch(patch, 'undo')
966
967         return ret
968
969     def pop_patch(self, name, keep = False):
970         """Pops the top patch from the stack
971         """
972         applied = self.get_applied()
973         applied.reverse()
974         assert(name in applied)
975
976         patch = Patch(name, self.__patch_dir, self.__refs_dir)
977
978         git.switch(patch.get_bottom(), keep)
979
980         # save the new applied list
981         idx = applied.index(name) + 1
982
983         popped = applied[:idx]
984         popped.reverse()
985         unapplied = popped + self.get_unapplied()
986
987         f = file(self.__unapplied_file, 'w+')
988         f.writelines([line + '\n' for line in unapplied])
989         f.close()
990
991         del applied[:idx]
992         applied.reverse()
993
994         f = file(self.__applied_file, 'w+')
995         f.writelines([line + '\n' for line in applied])
996         f.close()
997
998         if applied == []:
999             self.__set_current(None)
1000         else:
1001             self.__set_current(applied[-1])
1002
1003         self.__end_stack_check()
1004
1005     def empty_patch(self, name):
1006         """Returns True if the patch is empty
1007         """
1008         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1009         bottom = patch.get_bottom()
1010         top = patch.get_top()
1011
1012         if bottom == top:
1013             return True
1014         elif git.get_commit(top).get_tree() \
1015                  == git.get_commit(bottom).get_tree():
1016             return True
1017
1018         return False
1019
1020     def rename_patch(self, oldname, newname):
1021         applied = self.get_applied()
1022         unapplied = self.get_unapplied()
1023
1024         if oldname == newname:
1025             raise StackException, '"To" name and "from" name are the same'
1026
1027         if newname in applied or newname in unapplied:
1028             raise StackException, 'Patch "%s" already exists' % newname
1029
1030         if oldname in unapplied:
1031             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1032             unapplied[unapplied.index(oldname)] = newname
1033
1034             f = file(self.__unapplied_file, 'w+')
1035             f.writelines([line + '\n' for line in unapplied])
1036             f.close()
1037         elif oldname in applied:
1038             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1039             if oldname == self.get_current():
1040                 self.__set_current(newname)
1041
1042             applied[applied.index(oldname)] = newname
1043
1044             f = file(self.__applied_file, 'w+')
1045             f.writelines([line + '\n' for line in applied])
1046             f.close()
1047         else:
1048             raise StackException, 'Unknown patch "%s"' % oldname
1049
1050     def log_patch(self, patch, message):
1051         """Generate a log commit for a patch
1052         """
1053         top = git.get_commit(patch.get_top())
1054         msg = '%s\t%s' % (message, top.get_id_hash())
1055
1056         old_log = patch.get_log()
1057         if old_log:
1058             parents = [old_log]
1059         else:
1060             parents = []
1061
1062         log = git.commit(message = msg, parents = parents,
1063                          cache_update = False, tree_id = top.get_tree(),
1064                          allowempty = True)
1065         patch.set_log(log)