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