chiark / gitweb /
4df306a03927e98c4a2869c671baa28b5b951f72
[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         self.__set_field('authname', name or git.author().name)
242
243     def get_authemail(self):
244         return self.__get_field('authemail')
245
246     def set_authemail(self, email):
247         self.__set_field('authemail', email or git.author().email)
248
249     def get_authdate(self):
250         return self.__get_field('authdate')
251
252     def set_authdate(self, date):
253         self.__set_field('authdate', date or git.author().date)
254
255     def get_commname(self):
256         return self.__get_field('commname')
257
258     def set_commname(self, name):
259         self.__set_field('commname', name or git.committer().name)
260
261     def get_commemail(self):
262         return self.__get_field('commemail')
263
264     def set_commemail(self, email):
265         self.__set_field('commemail', email or git.committer().email)
266
267     def get_log(self):
268         return self.__get_field('log')
269
270     def set_log(self, value, backup = False):
271         self.__set_field('log', value)
272         self.__update_log_ref(value)
273
274
275 class Series:
276     """Class including the operations on series
277     """
278     def __init__(self, name = None):
279         """Takes a series name as the parameter.
280         """
281         try:
282             if name:
283                 self.__name = name
284             else:
285                 self.__name = git.get_head_file()
286             self.__base_dir = basedir.get()
287         except git.GitException, ex:
288             raise StackException, 'GIT tree not initialised: %s' % ex
289
290         self.__series_dir = os.path.join(self.__base_dir, 'patches',
291                                          self.__name)
292         self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
293                                        self.__name)
294         self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
295                                         self.__name)
296
297         self.__applied_file = os.path.join(self.__series_dir, 'applied')
298         self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied')
299         self.__current_file = os.path.join(self.__series_dir, 'current')
300         self.__descr_file = os.path.join(self.__series_dir, 'description')
301
302         # where this series keeps its patches
303         self.__patch_dir = os.path.join(self.__series_dir, 'patches')
304         if not os.path.isdir(self.__patch_dir):
305             self.__patch_dir = self.__series_dir
306
307         # if no __refs_dir, create and populate it (upgrade old repositories)
308         if self.is_initialised() and not os.path.isdir(self.__refs_dir):
309             os.makedirs(self.__refs_dir)
310             for patch in self.get_applied() + self.get_unapplied():
311                 self.get_patch(patch).update_top_ref()
312
313         # trash directory
314         self.__trash_dir = os.path.join(self.__series_dir, 'trash')
315         if self.is_initialised() and not os.path.isdir(self.__trash_dir):
316             os.makedirs(self.__trash_dir)
317
318     def get_branch(self):
319         """Return the branch name for the Series object
320         """
321         return self.__name
322
323     def __set_current(self, name):
324         """Sets the topmost patch
325         """
326         if name:
327             write_string(self.__current_file, name)
328         else:
329             create_empty_file(self.__current_file)
330
331     def get_patch(self, name):
332         """Return a Patch object for the given name
333         """
334         return Patch(name, self.__patch_dir, self.__refs_dir)
335
336     def get_current_patch(self):
337         """Return a Patch object representing the topmost patch, or
338         None if there is no such patch."""
339         crt = self.get_current()
340         if not crt:
341             return None
342         return Patch(crt, self.__patch_dir, self.__refs_dir)
343
344     def get_current(self):
345         """Return the name of the topmost patch, or None if there is
346         no such patch."""
347         if os.path.isfile(self.__current_file):
348             name = read_string(self.__current_file)
349         else:
350             return None
351         if name == '':
352             return None
353         else:
354             return name
355
356     def get_applied(self):
357         if not os.path.isfile(self.__applied_file):
358             raise StackException, 'Branch "%s" not initialised' % self.__name
359         f = file(self.__applied_file)
360         names = [line.strip() for line in f.readlines()]
361         f.close()
362         return names
363
364     def get_unapplied(self):
365         if not os.path.isfile(self.__unapplied_file):
366             raise StackException, 'Branch "%s" not initialised' % self.__name
367         f = file(self.__unapplied_file)
368         names = [line.strip() for line in f.readlines()]
369         f.close()
370         return names
371
372     def get_base_file(self):
373         self.__begin_stack_check()
374         return self.__base_file
375
376     def get_protected(self):
377         return os.path.isfile(os.path.join(self.__series_dir, 'protected'))
378
379     def protect(self):
380         protect_file = os.path.join(self.__series_dir, 'protected')
381         if not os.path.isfile(protect_file):
382             create_empty_file(protect_file)
383
384     def unprotect(self):
385         protect_file = os.path.join(self.__series_dir, 'protected')
386         if os.path.isfile(protect_file):
387             os.remove(protect_file)
388
389     def get_description(self):
390         if os.path.isfile(self.__descr_file):
391             return read_string(self.__descr_file)
392         else:
393             return ''
394
395     def __patch_is_current(self, patch):
396         return patch.get_name() == read_string(self.__current_file)
397
398     def patch_applied(self, name):
399         """Return true if the patch exists in the applied list
400         """
401         return name in self.get_applied()
402
403     def patch_unapplied(self, name):
404         """Return true if the patch exists in the unapplied list
405         """
406         return name in self.get_unapplied()
407
408     def patch_exists(self, name):
409         """Return true if there is a patch with the given name, false
410         otherwise."""
411         return self.patch_applied(name) or self.patch_unapplied(name)
412
413     def __begin_stack_check(self):
414         """Save the current HEAD into .git/refs/heads/base if the stack
415         is empty
416         """
417         if len(self.get_applied()) == 0:
418             head = git.get_head()
419             write_string(self.__base_file, head)
420
421     def __end_stack_check(self):
422         """Remove .git/refs/heads/base if the stack is empty.
423         This warning should never happen
424         """
425         if len(self.get_applied()) == 0 \
426            and read_string(self.__base_file) != git.get_head():
427             print 'Warning: stack empty but the HEAD and base are different'
428
429     def head_top_equal(self):
430         """Return true if the head and the top are the same
431         """
432         crt = self.get_current_patch()
433         if not crt:
434             # we don't care, no patches applied
435             return True
436         return git.get_head() == crt.get_top()
437
438     def is_initialised(self):
439         """Checks if series is already initialised
440         """
441         return os.path.isdir(self.__patch_dir)
442
443     def init(self, create_at=False):
444         """Initialises the stgit series
445         """
446         bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
447
448         if os.path.exists(self.__patch_dir):
449             raise StackException, self.__patch_dir + ' already exists'
450         if os.path.exists(self.__refs_dir):
451             raise StackException, self.__refs_dir + ' already exists'
452         if os.path.exists(self.__base_file):
453             raise StackException, self.__base_file + ' already exists'
454
455         if (create_at!=False):
456             git.create_branch(self.__name, create_at)
457
458         os.makedirs(self.__patch_dir)
459
460         create_dirs(bases_dir)
461
462         create_empty_file(self.__applied_file)
463         create_empty_file(self.__unapplied_file)
464         create_empty_file(self.__descr_file)
465         os.makedirs(os.path.join(self.__series_dir, 'patches'))
466         os.makedirs(self.__refs_dir)
467         self.__begin_stack_check()
468
469     def convert(self):
470         """Either convert to use a separate patch directory, or
471         unconvert to place the patches in the same directory with
472         series control files
473         """
474         if self.__patch_dir == self.__series_dir:
475             print 'Converting old-style to new-style...',
476             sys.stdout.flush()
477
478             self.__patch_dir = os.path.join(self.__series_dir, 'patches')
479             os.makedirs(self.__patch_dir)
480
481             for p in self.get_applied() + self.get_unapplied():
482                 src = os.path.join(self.__series_dir, p)
483                 dest = os.path.join(self.__patch_dir, p)
484                 os.rename(src, dest)
485
486             print 'done'
487
488         else:
489             print 'Converting new-style to old-style...',
490             sys.stdout.flush()
491
492             for p in self.get_applied() + self.get_unapplied():
493                 src = os.path.join(self.__patch_dir, p)
494                 dest = os.path.join(self.__series_dir, p)
495                 os.rename(src, dest)
496
497             if not os.listdir(self.__patch_dir):
498                 os.rmdir(self.__patch_dir)
499                 print 'done'
500             else:
501                 print 'Patch directory %s is not empty.' % self.__name
502
503             self.__patch_dir = self.__series_dir
504
505     def rename(self, to_name):
506         """Renames a series
507         """
508         to_stack = Series(to_name)
509
510         if to_stack.is_initialised():
511             raise StackException, '"%s" already exists' % to_stack.get_branch()
512         if os.path.exists(to_stack.__base_file):
513             os.remove(to_stack.__base_file)
514
515         git.rename_branch(self.__name, to_name)
516
517         if os.path.isdir(self.__series_dir):
518             rename(os.path.join(self.__base_dir, 'patches'),
519                    self.__name, to_stack.__name)
520         if os.path.exists(self.__base_file):
521             rename(os.path.join(self.__base_dir, 'refs', 'bases'),
522                    self.__name, to_stack.__name)
523         if os.path.exists(self.__refs_dir):
524             rename(os.path.join(self.__base_dir, 'refs', 'patches'),
525                    self.__name, to_stack.__name)
526
527         self.__init__(to_name)
528
529     def clone(self, target_series):
530         """Clones a series
531         """
532         base = read_string(self.get_base_file())
533         Series(target_series).init(create_at = base)
534         new_series = Series(target_series)
535
536         # generate an artificial description file
537         write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
538
539         # clone self's entire series as unapplied patches
540         patches = self.get_applied() + self.get_unapplied()
541         patches.reverse()
542         for p in patches:
543             patch = self.get_patch(p)
544             new_series.new_patch(p, message = patch.get_description(),
545                                  can_edit = False, unapplied = True,
546                                  bottom = patch.get_bottom(),
547                                  top = patch.get_top(),
548                                  author_name = patch.get_authname(),
549                                  author_email = patch.get_authemail(),
550                                  author_date = patch.get_authdate())
551
552         # fast forward the cloned series to self's top
553         new_series.forward_patches(self.get_applied())
554
555     def delete(self, force = False):
556         """Deletes an stgit series
557         """
558         if self.is_initialised():
559             patches = self.get_unapplied() + self.get_applied()
560             if not force and patches:
561                 raise StackException, \
562                       'Cannot delete: the series still contains patches'
563             for p in patches:
564                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
565
566             # remove the trash directory
567             for fname in os.listdir(self.__trash_dir):
568                 os.remove(fname)
569             os.rmdir(self.__trash_dir)
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, refresh = True):
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             if refresh:
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         # save the commit id to a trash file
761         write_string(os.path.join(self.__trash_dir, name), patch.get_top())
762
763         patch.delete()
764
765         unapplied = self.get_unapplied()
766         unapplied.remove(name)
767         f = file(self.__unapplied_file, 'w+')
768         f.writelines([line + '\n' for line in unapplied])
769         f.close()
770         self.__begin_stack_check()
771
772     def forward_patches(self, names):
773         """Try to fast-forward an array of patches.
774
775         On return, patches in names[0:returned_value] have been pushed on the
776         stack. Apply the rest with push_patch
777         """
778         unapplied = self.get_unapplied()
779         self.__begin_stack_check()
780
781         forwarded = 0
782         top = git.get_head()
783
784         for name in names:
785             assert(name in unapplied)
786
787             patch = Patch(name, self.__patch_dir, self.__refs_dir)
788
789             head = top
790             bottom = patch.get_bottom()
791             top = patch.get_top()
792
793             # top != bottom always since we have a commit for each patch
794             if head == bottom:
795                 # reset the backup information. No logging since the
796                 # patch hasn't changed
797                 patch.set_bottom(head, backup = True)
798                 patch.set_top(top, backup = True)
799
800             else:
801                 head_tree = git.get_commit(head).get_tree()
802                 bottom_tree = git.get_commit(bottom).get_tree()
803                 if head_tree == bottom_tree:
804                     # We must just reparent this patch and create a new commit
805                     # for it
806                     descr = patch.get_description()
807                     author_name = patch.get_authname()
808                     author_email = patch.get_authemail()
809                     author_date = patch.get_authdate()
810                     committer_name = patch.get_commname()
811                     committer_email = patch.get_commemail()
812
813                     top_tree = git.get_commit(top).get_tree()
814
815                     top = git.commit(message = descr, parents = [head],
816                                      cache_update = False,
817                                      tree_id = top_tree,
818                                      allowempty = True,
819                                      author_name = author_name,
820                                      author_email = author_email,
821                                      author_date = author_date,
822                                      committer_name = committer_name,
823                                      committer_email = committer_email)
824
825                     patch.set_bottom(head, backup = True)
826                     patch.set_top(top, backup = True)
827
828                     self.log_patch(patch, 'push(f)')
829                 else:
830                     top = head
831                     # stop the fast-forwarding, must do a real merge
832                     break
833
834             forwarded+=1
835             unapplied.remove(name)
836
837         if forwarded == 0:
838             return 0
839
840         git.switch(top)
841
842         append_strings(self.__applied_file, names[0:forwarded])
843
844         f = file(self.__unapplied_file, 'w+')
845         f.writelines([line + '\n' for line in unapplied])
846         f.close()
847
848         self.__set_current(name)
849
850         return forwarded
851
852     def merged_patches(self, names):
853         """Test which patches were merged upstream by reverse-applying
854         them in reverse order. The function returns the list of
855         patches detected to have been applied. The state of the tree
856         is restored to the original one
857         """
858         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
859                    for name in names]
860         patches.reverse()
861
862         merged = []
863         for p in patches:
864             if git.apply_diff(p.get_top(), p.get_bottom()):
865                 merged.append(p.get_name())
866         merged.reverse()
867
868         git.reset()
869
870         return merged
871
872     def push_patch(self, name, empty = False):
873         """Pushes a patch on the stack
874         """
875         unapplied = self.get_unapplied()
876         assert(name in unapplied)
877
878         self.__begin_stack_check()
879
880         patch = Patch(name, self.__patch_dir, self.__refs_dir)
881
882         head = git.get_head()
883         bottom = patch.get_bottom()
884         top = patch.get_top()
885
886         ex = None
887         modified = False
888
889         # top != bottom always since we have a commit for each patch
890         if empty:
891             # just make an empty patch (top = bottom = HEAD). This
892             # option is useful to allow undoing already merged
893             # patches. The top is updated by refresh_patch since we
894             # need an empty commit
895             patch.set_bottom(head, backup = True)
896             patch.set_top(head, backup = True)
897             modified = True
898         elif head == bottom:
899             # reset the backup information. No need for logging
900             patch.set_bottom(bottom, backup = True)
901             patch.set_top(top, backup = True)
902
903             git.switch(top)
904         else:
905             # new patch needs to be refreshed.
906             # The current patch is empty after merge.
907             patch.set_bottom(head, backup = True)
908             patch.set_top(head, backup = True)
909
910             # Try the fast applying first. If this fails, fall back to the
911             # three-way merge
912             if not git.apply_diff(bottom, top):
913                 # if git.apply_diff() fails, the patch requires a diff3
914                 # merge and can be reported as modified
915                 modified = True
916
917                 # merge can fail but the patch needs to be pushed
918                 try:
919                     git.merge(bottom, head, top)
920                 except git.GitException, ex:
921                     print >> sys.stderr, \
922                           'The merge failed during "push". ' \
923                           'Use "refresh" after fixing the conflicts'
924
925         append_string(self.__applied_file, name)
926
927         unapplied.remove(name)
928         f = file(self.__unapplied_file, 'w+')
929         f.writelines([line + '\n' for line in unapplied])
930         f.close()
931
932         self.__set_current(name)
933
934         # head == bottom case doesn't need to refresh the patch
935         if empty or head != bottom:
936             if not ex:
937                 # if the merge was OK and no conflicts, just refresh the patch
938                 # The GIT cache was already updated by the merge operation
939                 if modified:
940                     log = 'push(m)'
941                 else:
942                     log = 'push'
943                 self.refresh_patch(cache_update = False, log = log)
944             else:
945                 raise StackException, str(ex)
946
947         return modified
948
949     def undo_push(self):
950         name = self.get_current()
951         assert(name)
952
953         patch = Patch(name, self.__patch_dir, self.__refs_dir)
954         old_bottom = patch.get_old_bottom()
955         old_top = patch.get_old_top()
956
957         # the top of the patch is changed by a push operation only
958         # together with the bottom (otherwise the top was probably
959         # modified by 'refresh'). If they are both unchanged, there
960         # was a fast forward
961         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
962             raise StackException, 'No push undo information available'
963
964         git.reset()
965         self.pop_patch(name)
966         ret = patch.restore_old_boundaries()
967         if ret:
968             self.log_patch(patch, 'undo')
969
970         return ret
971
972     def pop_patch(self, name, keep = False):
973         """Pops the top patch from the stack
974         """
975         applied = self.get_applied()
976         applied.reverse()
977         assert(name in applied)
978
979         patch = Patch(name, self.__patch_dir, self.__refs_dir)
980
981         # only keep the local changes
982         if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
983             raise StackException, \
984                   'Failed to pop patches while preserving the local changes'
985
986         git.switch(patch.get_bottom(), keep)
987
988         # save the new applied list
989         idx = applied.index(name) + 1
990
991         popped = applied[:idx]
992         popped.reverse()
993         unapplied = popped + self.get_unapplied()
994
995         f = file(self.__unapplied_file, 'w+')
996         f.writelines([line + '\n' for line in unapplied])
997         f.close()
998
999         del applied[:idx]
1000         applied.reverse()
1001
1002         f = file(self.__applied_file, 'w+')
1003         f.writelines([line + '\n' for line in applied])
1004         f.close()
1005
1006         if applied == []:
1007             self.__set_current(None)
1008         else:
1009             self.__set_current(applied[-1])
1010
1011         self.__end_stack_check()
1012
1013     def empty_patch(self, name):
1014         """Returns True if the patch is empty
1015         """
1016         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1017         bottom = patch.get_bottom()
1018         top = patch.get_top()
1019
1020         if bottom == top:
1021             return True
1022         elif git.get_commit(top).get_tree() \
1023                  == git.get_commit(bottom).get_tree():
1024             return True
1025
1026         return False
1027
1028     def rename_patch(self, oldname, newname):
1029         applied = self.get_applied()
1030         unapplied = self.get_unapplied()
1031
1032         if oldname == newname:
1033             raise StackException, '"To" name and "from" name are the same'
1034
1035         if newname in applied or newname in unapplied:
1036             raise StackException, 'Patch "%s" already exists' % newname
1037
1038         if oldname in unapplied:
1039             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1040             unapplied[unapplied.index(oldname)] = newname
1041
1042             f = file(self.__unapplied_file, 'w+')
1043             f.writelines([line + '\n' for line in unapplied])
1044             f.close()
1045         elif oldname in applied:
1046             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1047             if oldname == self.get_current():
1048                 self.__set_current(newname)
1049
1050             applied[applied.index(oldname)] = newname
1051
1052             f = file(self.__applied_file, 'w+')
1053             f.writelines([line + '\n' for line in applied])
1054             f.close()
1055         else:
1056             raise StackException, 'Unknown patch "%s"' % oldname
1057
1058     def log_patch(self, patch, message):
1059         """Generate a log commit for a patch
1060         """
1061         top = git.get_commit(patch.get_top())
1062         msg = '%s\t%s' % (message, top.get_id_hash())
1063
1064         old_log = patch.get_log()
1065         if old_log:
1066             parents = [old_log]
1067         else:
1068             parents = []
1069
1070         log = git.commit(message = msg, parents = parents,
1071                          cache_update = False, tree_id = top.get_tree(),
1072                          allowempty = True)
1073         patch.set_log(log)