chiark / gitweb /
e1c55f0ac4efab49d727f0c1d576509c1f75846b
[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 push_patch(self, name):
784         """Pushes a patch on the stack
785         """
786         unapplied = self.get_unapplied()
787         assert(name in unapplied)
788
789         self.__begin_stack_check()
790
791         patch = Patch(name, self.__patch_dir, self.__refs_dir)
792
793         head = git.get_head()
794         bottom = patch.get_bottom()
795         top = patch.get_top()
796
797         ex = None
798         modified = False
799
800         # top != bottom always since we have a commit for each patch
801         if head == bottom:
802             # reset the backup information
803             patch.set_bottom(bottom, backup = True)
804             patch.set_top(top, backup = True)
805
806             git.switch(top)
807         else:
808             # new patch needs to be refreshed.
809             # The current patch is empty after merge.
810             patch.set_bottom(head, backup = True)
811             patch.set_top(head, backup = True)
812
813             # Try the fast applying first. If this fails, fall back to the
814             # three-way merge
815             if not git.apply_diff(bottom, top):
816                 # if git.apply_diff() fails, the patch requires a diff3
817                 # merge and can be reported as modified
818                 modified = True
819
820                 # merge can fail but the patch needs to be pushed
821                 try:
822                     git.merge(bottom, head, top)
823                 except git.GitException, ex:
824                     print >> sys.stderr, \
825                           'The merge failed during "push". ' \
826                           'Use "refresh" after fixing the conflicts'
827
828         append_string(self.__applied_file, name)
829
830         unapplied.remove(name)
831         f = file(self.__unapplied_file, 'w+')
832         f.writelines([line + '\n' for line in unapplied])
833         f.close()
834
835         self.__set_current(name)
836
837         # head == bottom case doesn't need to refresh the patch
838         if head != bottom:
839             if not ex:
840                 # if the merge was OK and no conflicts, just refresh the patch
841                 # The GIT cache was already updated by the merge operation
842                 self.refresh_patch(cache_update = False)
843             else:
844                 raise StackException, str(ex)
845
846         return modified
847
848     def undo_push(self):
849         name = self.get_current()
850         assert(name)
851
852         patch = Patch(name, self.__patch_dir, self.__refs_dir)
853         git.reset()
854         self.pop_patch(name)
855         return patch.restore_old_boundaries()
856
857     def pop_patch(self, name):
858         """Pops the top patch from the stack
859         """
860         applied = self.get_applied()
861         applied.reverse()
862         assert(name in applied)
863
864         patch = Patch(name, self.__patch_dir, self.__refs_dir)
865
866         git.switch(patch.get_bottom())
867
868         # save the new applied list
869         idx = applied.index(name) + 1
870
871         popped = applied[:idx]
872         popped.reverse()
873         unapplied = popped + self.get_unapplied()
874
875         f = file(self.__unapplied_file, 'w+')
876         f.writelines([line + '\n' for line in unapplied])
877         f.close()
878
879         del applied[:idx]
880         applied.reverse()
881
882         f = file(self.__applied_file, 'w+')
883         f.writelines([line + '\n' for line in applied])
884         f.close()
885
886         if applied == []:
887             self.__set_current(None)
888         else:
889             self.__set_current(applied[-1])
890
891         self.__end_stack_check()
892
893     def empty_patch(self, name):
894         """Returns True if the patch is empty
895         """
896         patch = Patch(name, self.__patch_dir, self.__refs_dir)
897         bottom = patch.get_bottom()
898         top = patch.get_top()
899
900         if bottom == top:
901             return True
902         elif git.get_commit(top).get_tree() \
903                  == git.get_commit(bottom).get_tree():
904             return True
905
906         return False
907
908     def rename_patch(self, oldname, newname):
909         applied = self.get_applied()
910         unapplied = self.get_unapplied()
911
912         if oldname == newname:
913             raise StackException, '"To" name and "from" name are the same'
914
915         if newname in applied or newname in unapplied:
916             raise StackException, 'Patch "%s" already exists' % newname
917
918         if oldname in unapplied:
919             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
920             unapplied[unapplied.index(oldname)] = newname
921
922             f = file(self.__unapplied_file, 'w+')
923             f.writelines([line + '\n' for line in unapplied])
924             f.close()
925         elif oldname in applied:
926             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
927             if oldname == self.get_current():
928                 self.__set_current(newname)
929
930             applied[applied.index(oldname)] = newname
931
932             f = file(self.__applied_file, 'w+')
933             f.writelines([line + '\n' for line in applied])
934             f.close()
935         else:
936             raise StackException, 'Unknown patch "%s"' % oldname