chiark / gitweb /
common: parse 'email (name)' correctly
[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
25 from stgit.config import config
26
27
28 # stack exception class
29 class StackException(Exception):
30     pass
31
32 class FilterUntil:
33     def __init__(self):
34         self.should_print = True
35     def __call__(self, x, until_test, prefix):
36         if until_test(x):
37             self.should_print = False
38         if self.should_print:
39             return x[0:len(prefix)] != prefix
40         return False
41
42 #
43 # Functions
44 #
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
47
48 def __clean_comments(f):
49     """Removes lines marked for status in a commit file
50     """
51     f.seek(0)
52
53     # remove status-prefixed lines
54     lines = f.readlines()
55
56     patch_filter = FilterUntil()
57     until_test = lambda t: t == (__patch_prefix + '\n')
58     lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
59
60     # remove empty lines at the end
61     while len(lines) != 0 and lines[-1] == '\n':
62         del lines[-1]
63
64     f.seek(0); f.truncate()
65     f.writelines(lines)
66
67 def edit_file(series, line, comment, show_patch = True):
68     fname = '.stgit.msg'
69     tmpl = os.path.join(git.get_base_dir(), 'patchdescr.tmpl')
70
71     f = file(fname, 'w+')
72     if line:
73         print >> f, line
74     elif os.path.isfile(tmpl):
75         print >> f, file(tmpl).read().rstrip()
76     else:
77         print >> f
78     print >> f, __comment_prefix, comment
79     print >> f, __comment_prefix, \
80           'Lines prefixed with "%s" will be automatically removed.' \
81           % __comment_prefix
82     print >> f, __comment_prefix, \
83           'Trailing empty lines will be automatically removed.'
84
85     if show_patch:
86        print >> f, __patch_prefix
87        # series.get_patch(series.get_current()).get_top()
88        git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
89
90     #Vim modeline must be near the end.
91     print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
92     f.close()
93
94     # the editor
95     if config.has_option('stgit', 'editor'):
96         editor = config.get('stgit', 'editor')
97     elif 'EDITOR' in os.environ:
98         editor = os.environ['EDITOR']
99     else:
100         editor = 'vi'
101     editor += ' %s' % fname
102
103     print 'Invoking the editor: "%s"...' % editor,
104     sys.stdout.flush()
105     print 'done (exit code: %d)' % os.system(editor)
106
107     f = file(fname, 'r+')
108
109     __clean_comments(f)
110     f.seek(0)
111     result = f.read()
112
113     f.close()
114     os.remove(fname)
115
116     return result
117
118 #
119 # Classes
120 #
121
122 class Patch:
123     """Basic patch implementation
124     """
125     def __init__(self, name, series_dir, refs_dir):
126         self.__series_dir = series_dir
127         self.__name = name
128         self.__dir = os.path.join(self.__series_dir, self.__name)
129         self.__refs_dir = refs_dir
130         self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
131
132     def create(self):
133         os.mkdir(self.__dir)
134         create_empty_file(os.path.join(self.__dir, 'bottom'))
135         create_empty_file(os.path.join(self.__dir, 'top'))
136
137     def delete(self):
138         for f in os.listdir(self.__dir):
139             os.remove(os.path.join(self.__dir, f))
140         os.rmdir(self.__dir)
141         os.remove(self.__top_ref_file)
142
143     def get_name(self):
144         return self.__name
145
146     def rename(self, newname):
147         olddir = self.__dir
148         old_ref_file = self.__top_ref_file
149         self.__name = newname
150         self.__dir = os.path.join(self.__series_dir, self.__name)
151         self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
152
153         os.rename(olddir, self.__dir)
154         os.rename(old_ref_file, self.__top_ref_file)
155
156     def __update_top_ref(self, ref):
157         write_string(self.__top_ref_file, ref)
158
159     def update_top_ref(self):
160         top = self.get_top()
161         if top:
162             self.__update_top_ref(top)
163
164     def __get_field(self, name, multiline = False):
165         id_file = os.path.join(self.__dir, name)
166         if os.path.isfile(id_file):
167             line = read_string(id_file, multiline)
168             if line == '':
169                 return None
170             else:
171                 return line
172         else:
173             return None
174
175     def __set_field(self, name, value, multiline = False):
176         fname = os.path.join(self.__dir, name)
177         if value and value != '':
178             write_string(fname, value, multiline)
179         elif os.path.isfile(fname):
180             os.remove(fname)
181
182     def get_old_bottom(self):
183         return self.__get_field('bottom.old')
184
185     def get_bottom(self):
186         return self.__get_field('bottom')
187
188     def set_bottom(self, value, backup = False):
189         if backup:
190             curr = self.__get_field('bottom')
191             if curr != value:
192                 self.__set_field('bottom.old', curr)
193             else:
194                 self.__set_field('bottom.old', None)
195         self.__set_field('bottom', value)
196
197     def get_old_top(self):
198         return self.__get_field('top.old')
199
200     def get_top(self):
201         return self.__get_field('top')
202
203     def set_top(self, value, backup = False):
204         if backup:
205             curr = self.__get_field('top')
206             if curr != value:
207                 self.__set_field('top.old', curr)
208             else:
209                 self.__set_field('top.old', None)
210         self.__set_field('top', value)
211         self.__update_top_ref(value)
212
213     def restore_old_boundaries(self):
214         bottom = self.__get_field('bottom.old')
215         top = self.__get_field('top.old')
216
217         if top and bottom:
218             self.__set_field('bottom', bottom)
219             self.__set_field('top', top)
220             self.__update_top_ref(top)
221             return True
222         else:
223             return False
224
225     def get_description(self):
226         return self.__get_field('description', True)
227
228     def set_description(self, line):
229         self.__set_field('description', line, True)
230
231     def get_authname(self):
232         return self.__get_field('authname')
233
234     def set_authname(self, name):
235         if not name:
236             if config.has_option('stgit', 'authname'):
237                 name = config.get('stgit', 'authname')
238             elif 'GIT_AUTHOR_NAME' in os.environ:
239                 name = os.environ['GIT_AUTHOR_NAME']
240         self.__set_field('authname', name)
241
242     def get_authemail(self):
243         return self.__get_field('authemail')
244
245     def set_authemail(self, address):
246         if not address:
247             if config.has_option('stgit', 'authemail'):
248                 address = config.get('stgit', 'authemail')
249             elif 'GIT_AUTHOR_EMAIL' in os.environ:
250                 address = os.environ['GIT_AUTHOR_EMAIL']
251         self.__set_field('authemail', address)
252
253     def get_authdate(self):
254         return self.__get_field('authdate')
255
256     def set_authdate(self, date):
257         if not date and 'GIT_AUTHOR_DATE' in os.environ:
258             date = os.environ['GIT_AUTHOR_DATE']
259         self.__set_field('authdate', date)
260
261     def get_commname(self):
262         return self.__get_field('commname')
263
264     def set_commname(self, name):
265         if not name:
266             if config.has_option('stgit', 'commname'):
267                 name = config.get('stgit', 'commname')
268             elif 'GIT_COMMITTER_NAME' in os.environ:
269                 name = os.environ['GIT_COMMITTER_NAME']
270         self.__set_field('commname', name)
271
272     def get_commemail(self):
273         return self.__get_field('commemail')
274
275     def set_commemail(self, address):
276         if not address:
277             if config.has_option('stgit', 'commemail'):
278                 address = config.get('stgit', 'commemail')
279             elif 'GIT_COMMITTER_EMAIL' in os.environ:
280                 address = os.environ['GIT_COMMITTER_EMAIL']
281         self.__set_field('commemail', address)
282
283
284 class Series:
285     """Class including the operations on series
286     """
287     def __init__(self, name = None):
288         """Takes a series name as the parameter.
289         """
290         try:
291             if name:
292                 self.__name = name
293             else:
294                 self.__name = git.get_head_file()
295             self.__base_dir = git.get_base_dir()
296         except git.GitException, ex:
297             raise StackException, 'GIT tree not initialised: %s' % ex
298
299         self.__series_dir = os.path.join(self.__base_dir, 'patches',
300                                          self.__name)
301         self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
302                                        self.__name)
303         self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
304                                         self.__name)
305
306         self.__applied_file = os.path.join(self.__series_dir, 'applied')
307         self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied')
308         self.__current_file = os.path.join(self.__series_dir, 'current')
309         self.__descr_file = os.path.join(self.__series_dir, 'description')
310
311         # where this series keeps its patches
312         self.__patch_dir = os.path.join(self.__series_dir, 'patches')
313         if not os.path.isdir(self.__patch_dir):
314             self.__patch_dir = self.__series_dir
315
316         # if no __refs_dir, create and populate it (upgrade old repositories)
317         if self.is_initialised() and not os.path.isdir(self.__refs_dir):
318             os.makedirs(self.__refs_dir)
319             for patch in self.get_applied() + self.get_unapplied():
320                 self.get_patch(patch).update_top_ref()
321
322     def get_branch(self):
323         """Return the branch name for the Series object
324         """
325         return self.__name
326
327     def __set_current(self, name):
328         """Sets the topmost patch
329         """
330         if name:
331             write_string(self.__current_file, name)
332         else:
333             create_empty_file(self.__current_file)
334
335     def get_patch(self, name):
336         """Return a Patch object for the given name
337         """
338         return Patch(name, self.__patch_dir, self.__refs_dir)
339
340     def get_current(self):
341         """Return a Patch object representing the topmost patch
342         """
343         if os.path.isfile(self.__current_file):
344             name = read_string(self.__current_file)
345         else:
346             return None
347         if name == '':
348             return None
349         else:
350             return name
351
352     def get_applied(self):
353         if not os.path.isfile(self.__applied_file):
354             raise StackException, 'Branch "%s" not initialised' % self.__name
355         f = file(self.__applied_file)
356         names = [line.strip() for line in f.readlines()]
357         f.close()
358         return names
359
360     def get_unapplied(self):
361         if not os.path.isfile(self.__unapplied_file):
362             raise StackException, 'Branch "%s" not initialised' % self.__name
363         f = file(self.__unapplied_file)
364         names = [line.strip() for line in f.readlines()]
365         f.close()
366         return names
367
368     def get_base_file(self):
369         self.__begin_stack_check()
370         return self.__base_file
371
372     def get_protected(self):
373         return os.path.isfile(os.path.join(self.__series_dir, 'protected'))
374
375     def protect(self):
376         protect_file = os.path.join(self.__series_dir, 'protected')
377         if not os.path.isfile(protect_file):
378             create_empty_file(protect_file)
379
380     def unprotect(self):
381         protect_file = os.path.join(self.__series_dir, 'protected')
382         if os.path.isfile(protect_file):
383             os.remove(protect_file)
384
385     def get_description(self):
386         if os.path.isfile(self.__descr_file):
387             return read_string(self.__descr_file)
388         else:
389             return ''
390
391     def __patch_is_current(self, patch):
392         return patch.get_name() == read_string(self.__current_file)
393
394     def __patch_applied(self, name):
395         """Return true if the patch exists in the applied list
396         """
397         return name in self.get_applied()
398
399     def __patch_unapplied(self, name):
400         """Return true if the patch exists in the unapplied list
401         """
402         return name in self.get_unapplied()
403
404     def __begin_stack_check(self):
405         """Save the current HEAD into .git/refs/heads/base if the stack
406         is empty
407         """
408         if len(self.get_applied()) == 0:
409             head = git.get_head()
410             write_string(self.__base_file, head)
411
412     def __end_stack_check(self):
413         """Remove .git/refs/heads/base if the stack is empty.
414         This warning should never happen
415         """
416         if len(self.get_applied()) == 0 \
417            and read_string(self.__base_file) != git.get_head():
418             print 'Warning: stack empty but the HEAD and base are different'
419
420     def head_top_equal(self):
421         """Return true if the head and the top are the same
422         """
423         crt = self.get_current()
424         if not crt:
425             # we don't care, no patches applied
426             return True
427         return git.get_head() == Patch(crt, self.__patch_dir,
428                                        self.__refs_dir).get_top()
429
430     def is_initialised(self):
431         """Checks if series is already initialised
432         """
433         return os.path.isdir(self.__patch_dir)
434
435     def init(self):
436         """Initialises the stgit series
437         """
438         bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
439
440         if self.is_initialised():
441             raise StackException, self.__patch_dir + ' already exists'
442         os.makedirs(self.__patch_dir)
443
444         if not os.path.isdir(bases_dir):
445             os.makedirs(bases_dir)
446
447         create_empty_file(self.__applied_file)
448         create_empty_file(self.__unapplied_file)
449         create_empty_file(self.__descr_file)
450         os.makedirs(os.path.join(self.__series_dir, 'patches'))
451         os.makedirs(self.__refs_dir)
452         self.__begin_stack_check()
453
454     def convert(self):
455         """Either convert to use a separate patch directory, or
456         unconvert to place the patches in the same directory with
457         series control files
458         """
459         if self.__patch_dir == self.__series_dir:
460             print 'Converting old-style to new-style...',
461             sys.stdout.flush()
462
463             self.__patch_dir = os.path.join(self.__series_dir, 'patches')
464             os.makedirs(self.__patch_dir)
465
466             for p in self.get_applied() + self.get_unapplied():
467                 src = os.path.join(self.__series_dir, p)
468                 dest = os.path.join(self.__patch_dir, p)
469                 os.rename(src, dest)
470
471             print 'done'
472
473         else:
474             print 'Converting new-style to old-style...',
475             sys.stdout.flush()
476
477             for p in self.get_applied() + self.get_unapplied():
478                 src = os.path.join(self.__patch_dir, p)
479                 dest = os.path.join(self.__series_dir, p)
480                 os.rename(src, dest)
481
482             if not os.listdir(self.__patch_dir):
483                 os.rmdir(self.__patch_dir)
484                 print 'done'
485             else:
486                 print 'Patch directory %s is not empty.' % self.__name
487
488             self.__patch_dir = self.__series_dir
489
490     def rename(self, to_name):
491         """Renames a series
492         """
493         to_stack = Series(to_name)
494
495         if to_stack.is_initialised():
496             raise StackException, '"%s" already exists' % to_stack.get_branch()
497         if os.path.exists(to_stack.__base_file):
498             os.remove(to_stack.__base_file)
499
500         git.rename_branch(self.__name, to_name)
501
502         if os.path.isdir(self.__series_dir):
503             os.rename(self.__series_dir, to_stack.__series_dir)
504         if os.path.exists(self.__base_file):
505             os.rename(self.__base_file, to_stack.__base_file)
506
507         self.__init__(to_name)
508
509     def clone(self, target_series):
510         """Clones a series
511         """
512         base = read_string(self.get_base_file())
513         git.create_branch(target_series, tree_id = base)
514         Series(target_series).init()
515         new_series = Series(target_series)
516
517         # generate an artificial description file
518         write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
519
520         # clone self's entire series as unapplied patches
521         patches = self.get_applied() + self.get_unapplied()
522         patches.reverse()
523         for p in patches:
524             patch = self.get_patch(p)
525             new_series.new_patch(p, message = patch.get_description(),
526                                  can_edit = False, unapplied = True,
527                                  bottom = patch.get_bottom(),
528                                  top = patch.get_top(),
529                                  author_name = patch.get_authname(),
530                                  author_email = patch.get_authemail(),
531                                  author_date = patch.get_authdate())
532
533         # fast forward the cloned series to self's top
534         new_series.forward_patches(self.get_applied())
535
536     def delete(self, force = False):
537         """Deletes an stgit series
538         """
539         if self.is_initialised():
540             patches = self.get_unapplied() + self.get_applied()
541             if not force and patches:
542                 raise StackException, \
543                       'Cannot delete: the series still contains patches'
544             for p in patches:
545                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
546
547             if os.path.exists(self.__applied_file):
548                 os.remove(self.__applied_file)
549             if os.path.exists(self.__unapplied_file):
550                 os.remove(self.__unapplied_file)
551             if os.path.exists(self.__current_file):
552                 os.remove(self.__current_file)
553             if os.path.exists(self.__descr_file):
554                 os.remove(self.__descr_file)
555             if not os.listdir(self.__patch_dir):
556                 os.rmdir(self.__patch_dir)
557             else:
558                 print 'Patch directory %s is not empty.' % self.__name
559             if not os.listdir(self.__series_dir):
560                 os.rmdir(self.__series_dir)
561             else:
562                 print 'Series directory %s is not empty.' % self.__name
563             if not os.listdir(self.__refs_dir):
564                 os.rmdir(self.__refs_dir)
565             else:
566                 print 'Refs directory %s is not empty.' % self.__refs_dir
567
568         if os.path.exists(self.__base_file):
569             os.remove(self.__base_file)
570
571     def refresh_patch(self, files = None, message = None, edit = False,
572                       show_patch = False,
573                       cache_update = True,
574                       author_name = None, author_email = None,
575                       author_date = None,
576                       committer_name = None, committer_email = None):
577         """Generates a new commit for the given patch
578         """
579         name = self.get_current()
580         if not name:
581             raise StackException, 'No patches applied'
582
583         patch = Patch(name, self.__patch_dir, self.__refs_dir)
584
585         descr = patch.get_description()
586         if not (message or descr):
587             edit = True
588             descr = ''
589         elif message:
590             descr = message
591
592         if not message and edit:
593             descr = edit_file(self, descr.rstrip(), \
594                               'Please edit the description for patch "%s" ' \
595                               'above.' % name, show_patch)
596
597         if not author_name:
598             author_name = patch.get_authname()
599         if not author_email:
600             author_email = patch.get_authemail()
601         if not author_date:
602             author_date = patch.get_authdate()
603         if not committer_name:
604             committer_name = patch.get_commname()
605         if not committer_email:
606             committer_email = patch.get_commemail()
607
608         commit_id = git.commit(files = files,
609                                message = descr, parents = [patch.get_bottom()],
610                                cache_update = cache_update,
611                                allowempty = True,
612                                author_name = author_name,
613                                author_email = author_email,
614                                author_date = author_date,
615                                committer_name = committer_name,
616                                committer_email = committer_email)
617
618         patch.set_top(commit_id)
619         patch.set_description(descr)
620         patch.set_authname(author_name)
621         patch.set_authemail(author_email)
622         patch.set_authdate(author_date)
623         patch.set_commname(committer_name)
624         patch.set_commemail(committer_email)
625
626         return commit_id
627
628     def new_patch(self, name, message = None, can_edit = True,
629                   unapplied = False, show_patch = False,
630                   top = None, bottom = None,
631                   author_name = None, author_email = None, author_date = None,
632                   committer_name = None, committer_email = None,
633                   before_existing = False):
634         """Creates a new patch
635         """
636         if self.__patch_applied(name) or self.__patch_unapplied(name):
637             raise StackException, 'Patch "%s" already exists' % name
638
639         if not message and can_edit:
640             descr = edit_file(self, None, \
641                               'Please enter the description for patch "%s" ' \
642                               'above.' % name, show_patch)
643         else:
644             descr = message
645
646         head = git.get_head()
647
648         self.__begin_stack_check()
649
650         patch = Patch(name, self.__patch_dir, self.__refs_dir)
651         patch.create()
652
653         if bottom:
654             patch.set_bottom(bottom)
655         else:
656             patch.set_bottom(head)
657         if top:
658             patch.set_top(top)
659         else:
660             patch.set_top(head)
661
662         patch.set_description(descr)
663         patch.set_authname(author_name)
664         patch.set_authemail(author_email)
665         patch.set_authdate(author_date)
666         patch.set_commname(committer_name)
667         patch.set_commemail(committer_email)
668
669         if unapplied:
670             patches = [patch.get_name()] + self.get_unapplied()
671
672             f = file(self.__unapplied_file, 'w+')
673             f.writelines([line + '\n' for line in patches])
674             f.close()
675         else:
676             if before_existing:
677                 insert_string(self.__applied_file, patch.get_name())
678                 if not self.get_current():
679                     self.__set_current(name)
680             else:
681                 append_string(self.__applied_file, patch.get_name())
682                 self.__set_current(name)
683
684     def delete_patch(self, name):
685         """Deletes a patch
686         """
687         patch = Patch(name, self.__patch_dir, self.__refs_dir)
688
689         if self.__patch_is_current(patch):
690             self.pop_patch(name)
691         elif self.__patch_applied(name):
692             raise StackException, 'Cannot remove an applied patch, "%s", ' \
693                   'which is not current' % name
694         elif not name in self.get_unapplied():
695             raise StackException, 'Unknown patch "%s"' % name
696
697         patch.delete()
698
699         unapplied = self.get_unapplied()
700         unapplied.remove(name)
701         f = file(self.__unapplied_file, 'w+')
702         f.writelines([line + '\n' for line in unapplied])
703         f.close()
704         self.__begin_stack_check()
705
706     def forward_patches(self, names):
707         """Try to fast-forward an array of patches.
708
709         On return, patches in names[0:returned_value] have been pushed on the
710         stack. Apply the rest with push_patch
711         """
712         unapplied = self.get_unapplied()
713         self.__begin_stack_check()
714
715         forwarded = 0
716         top = git.get_head()
717
718         for name in names:
719             assert(name in unapplied)
720
721             patch = Patch(name, self.__patch_dir, self.__refs_dir)
722
723             head = top
724             bottom = patch.get_bottom()
725             top = patch.get_top()
726
727             # top != bottom always since we have a commit for each patch
728             if head == bottom:
729                 # reset the backup information
730                 patch.set_bottom(head, backup = True)
731                 patch.set_top(top, backup = True)
732
733             else:
734                 head_tree = git.get_commit(head).get_tree()
735                 bottom_tree = git.get_commit(bottom).get_tree()
736                 if head_tree == bottom_tree:
737                     # We must just reparent this patch and create a new commit
738                     # for it
739                     descr = patch.get_description()
740                     author_name = patch.get_authname()
741                     author_email = patch.get_authemail()
742                     author_date = patch.get_authdate()
743                     committer_name = patch.get_commname()
744                     committer_email = patch.get_commemail()
745
746                     top_tree = git.get_commit(top).get_tree()
747
748                     top = git.commit(message = descr, parents = [head],
749                                      cache_update = False,
750                                      tree_id = top_tree,
751                                      allowempty = True,
752                                      author_name = author_name,
753                                      author_email = author_email,
754                                      author_date = author_date,
755                                      committer_name = committer_name,
756                                      committer_email = committer_email)
757
758                     patch.set_bottom(head, backup = True)
759                     patch.set_top(top, backup = True)
760                 else:
761                     top = head
762                     # stop the fast-forwarding, must do a real merge
763                     break
764
765             forwarded+=1
766             unapplied.remove(name)
767
768         if forwarded == 0:
769             return 0
770
771         git.switch(top)
772
773         append_strings(self.__applied_file, names[0:forwarded])
774
775         f = file(self.__unapplied_file, 'w+')
776         f.writelines([line + '\n' for line in unapplied])
777         f.close()
778
779         self.__set_current(name)
780
781         return forwarded
782
783     def merged_patches(self, names):
784         """Test which patches were merged upstream by reverse-applying
785         them in reverse order. The function returns the list of
786         patches detected to have been applied. The state of the tree
787         is restored to the original one
788         """
789         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
790                    for name in names]
791         patches.reverse()
792
793         merged = []
794         for p in patches:
795             if git.apply_diff(p.get_top(), p.get_bottom(), False):
796                 merged.append(p.get_name())
797         merged.reverse()
798
799         git.reset()
800
801         return merged
802
803     def push_patch(self, name, empty = False):
804         """Pushes a patch on the stack
805         """
806         unapplied = self.get_unapplied()
807         assert(name in unapplied)
808
809         self.__begin_stack_check()
810
811         patch = Patch(name, self.__patch_dir, self.__refs_dir)
812
813         head = git.get_head()
814         bottom = patch.get_bottom()
815         top = patch.get_top()
816
817         ex = None
818         modified = False
819
820         # top != bottom always since we have a commit for each patch
821         if empty:
822             # just make an empty patch (top = bottom = HEAD). This
823             # option is useful to allow undoing already merged
824             # patches. The top is updated by refresh_patch since we
825             # need an empty commit
826             patch.set_bottom(head, backup = True)
827             patch.set_top(head, backup = True)
828             modified = True
829         elif head == bottom:
830             # reset the backup information
831             patch.set_bottom(bottom, backup = True)
832             patch.set_top(top, backup = True)
833
834             git.switch(top)
835         else:
836             # new patch needs to be refreshed.
837             # The current patch is empty after merge.
838             patch.set_bottom(head, backup = True)
839             patch.set_top(head, backup = True)
840
841             # Try the fast applying first. If this fails, fall back to the
842             # three-way merge
843             if not git.apply_diff(bottom, top):
844                 # if git.apply_diff() fails, the patch requires a diff3
845                 # merge and can be reported as modified
846                 modified = True
847
848                 # merge can fail but the patch needs to be pushed
849                 try:
850                     git.merge(bottom, head, top)
851                 except git.GitException, ex:
852                     print >> sys.stderr, \
853                           'The merge failed during "push". ' \
854                           'Use "refresh" after fixing the conflicts'
855
856         append_string(self.__applied_file, name)
857
858         unapplied.remove(name)
859         f = file(self.__unapplied_file, 'w+')
860         f.writelines([line + '\n' for line in unapplied])
861         f.close()
862
863         self.__set_current(name)
864
865         # head == bottom case doesn't need to refresh the patch
866         if empty or head != bottom:
867             if not ex:
868                 # if the merge was OK and no conflicts, just refresh the patch
869                 # The GIT cache was already updated by the merge operation
870                 self.refresh_patch(cache_update = False)
871             else:
872                 raise StackException, str(ex)
873
874         return modified
875
876     def undo_push(self):
877         name = self.get_current()
878         assert(name)
879
880         patch = Patch(name, self.__patch_dir, self.__refs_dir)
881         git.reset()
882         self.pop_patch(name)
883         return patch.restore_old_boundaries()
884
885     def pop_patch(self, name):
886         """Pops the top patch from the stack
887         """
888         applied = self.get_applied()
889         applied.reverse()
890         assert(name in applied)
891
892         patch = Patch(name, self.__patch_dir, self.__refs_dir)
893
894         git.switch(patch.get_bottom())
895
896         # save the new applied list
897         idx = applied.index(name) + 1
898
899         popped = applied[:idx]
900         popped.reverse()
901         unapplied = popped + self.get_unapplied()
902
903         f = file(self.__unapplied_file, 'w+')
904         f.writelines([line + '\n' for line in unapplied])
905         f.close()
906
907         del applied[:idx]
908         applied.reverse()
909
910         f = file(self.__applied_file, 'w+')
911         f.writelines([line + '\n' for line in applied])
912         f.close()
913
914         if applied == []:
915             self.__set_current(None)
916         else:
917             self.__set_current(applied[-1])
918
919         self.__end_stack_check()
920
921     def empty_patch(self, name):
922         """Returns True if the patch is empty
923         """
924         patch = Patch(name, self.__patch_dir, self.__refs_dir)
925         bottom = patch.get_bottom()
926         top = patch.get_top()
927
928         if bottom == top:
929             return True
930         elif git.get_commit(top).get_tree() \
931                  == git.get_commit(bottom).get_tree():
932             return True
933
934         return False
935
936     def rename_patch(self, oldname, newname):
937         applied = self.get_applied()
938         unapplied = self.get_unapplied()
939
940         if oldname == newname:
941             raise StackException, '"To" name and "from" name are the same'
942
943         if newname in applied or newname in unapplied:
944             raise StackException, 'Patch "%s" already exists' % newname
945
946         if oldname in unapplied:
947             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
948             unapplied[unapplied.index(oldname)] = newname
949
950             f = file(self.__unapplied_file, 'w+')
951             f.writelines([line + '\n' for line in unapplied])
952             f.close()
953         elif oldname in applied:
954             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
955             if oldname == self.get_current():
956                 self.__set_current(newname)
957
958             applied[applied.index(oldname)] = newname
959
960             f = file(self.__applied_file, 'w+')
961             f.writelines([line + '\n' for line in applied])
962             f.close()
963         else:
964             raise StackException, 'Unknown patch "%s"' % oldname