chiark / gitweb /
5237084ce04bf00e893599df0aacbb7a9ff0840b
[stgit] / stgit / stack.py
1 """Basic quilt-like functionality
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
20
21 import sys, os
22
23 from stgit.utils import *
24 from stgit import git, basedir, templates
25 from stgit.config import config
26
27
28 # stack exception class
29 class StackException(Exception):
30     pass
31
32 class FilterUntil:
33     def __init__(self):
34         self.should_print = True
35     def __call__(self, x, until_test, prefix):
36         if until_test(x):
37             self.should_print = False
38         if self.should_print:
39             return x[0:len(prefix)] != prefix
40         return False
41
42 #
43 # Functions
44 #
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
47
48 def __clean_comments(f):
49     """Removes lines marked for status in a commit file
50     """
51     f.seek(0)
52
53     # remove status-prefixed lines
54     lines = f.readlines()
55
56     patch_filter = FilterUntil()
57     until_test = lambda t: t == (__patch_prefix + '\n')
58     lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
59
60     # remove empty lines at the end
61     while len(lines) != 0 and lines[-1] == '\n':
62         del lines[-1]
63
64     f.seek(0); f.truncate()
65     f.writelines(lines)
66
67 def edit_file(series, line, comment, show_patch = True):
68     fname = '.stgitmsg.txt'
69     tmpl = templates.get_template('patchdescr.tmpl')
70
71     f = file(fname, 'w+')
72     if line:
73         print >> f, line
74     elif tmpl:
75         print >> f, tmpl,
76     else:
77         print >> f
78     print >> f, __comment_prefix, comment
79     print >> f, __comment_prefix, \
80           'Lines prefixed with "%s" will be automatically removed.' \
81           % __comment_prefix
82     print >> f, __comment_prefix, \
83           'Trailing empty lines will be automatically removed.'
84
85     if show_patch:
86        print >> f, __patch_prefix
87        # series.get_patch(series.get_current()).get_top()
88        git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
89
90     #Vim modeline must be near the end.
91     print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
92     f.close()
93
94     # the editor
95     if config.has_option('stgit', 'editor'):
96         editor = config.get('stgit', 'editor')
97     elif 'EDITOR' in os.environ:
98         editor = os.environ['EDITOR']
99     else:
100         editor = 'vi'
101     editor += ' %s' % fname
102
103     print 'Invoking the editor: "%s"...' % editor,
104     sys.stdout.flush()
105     print 'done (exit code: %d)' % os.system(editor)
106
107     f = file(fname, 'r+')
108
109     __clean_comments(f)
110     f.seek(0)
111     result = f.read()
112
113     f.close()
114     os.remove(fname)
115
116     return result
117
118 #
119 # Classes
120 #
121
122 class StgitObject:
123     """An object with stgit-like properties stored as files in a directory
124     """
125     def _set_dir(self, dir):
126         self.__dir = dir
127     def _dir(self):
128         return self.__dir
129
130     def create_empty_field(self, name):
131         create_empty_file(os.path.join(self.__dir, name))
132
133     def _get_field(self, name, multiline = False):
134         id_file = os.path.join(self.__dir, name)
135         if os.path.isfile(id_file):
136             line = read_string(id_file, multiline)
137             if line == '':
138                 return None
139             else:
140                 return line
141         else:
142             return None
143
144     def _set_field(self, name, value, multiline = False):
145         fname = os.path.join(self.__dir, name)
146         if value and value != '':
147             write_string(fname, value, multiline)
148         elif os.path.isfile(fname):
149             os.remove(fname)
150
151     
152 class Patch(StgitObject):
153     """Basic patch implementation
154     """
155     def __init__(self, name, series_dir, refs_dir):
156         self.__series_dir = series_dir
157         self.__name = name
158         self._set_dir(os.path.join(self.__series_dir, self.__name))
159         self.__refs_dir = refs_dir
160         self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
161         self.__log_ref_file = os.path.join(self.__refs_dir,
162                                            self.__name + '.log')
163
164     def create(self):
165         os.mkdir(self._dir())
166         self.create_empty_field('bottom')
167         self.create_empty_field('top')
168
169     def delete(self):
170         for f in os.listdir(self._dir()):
171             os.remove(os.path.join(self._dir(), f))
172         os.rmdir(self._dir())
173         os.remove(self.__top_ref_file)
174         if os.path.exists(self.__log_ref_file):
175             os.remove(self.__log_ref_file)
176
177     def get_name(self):
178         return self.__name
179
180     def rename(self, newname):
181         olddir = self._dir()
182         old_top_ref_file = self.__top_ref_file
183         old_log_ref_file = self.__log_ref_file
184         self.__name = newname
185         self._set_dir(os.path.join(self.__series_dir, self.__name))
186         self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
187         self.__log_ref_file = os.path.join(self.__refs_dir,
188                                            self.__name + '.log')
189
190         os.rename(olddir, self._dir())
191         os.rename(old_top_ref_file, self.__top_ref_file)
192         if os.path.exists(old_log_ref_file):
193             os.rename(old_log_ref_file, self.__log_ref_file)
194
195     def __update_top_ref(self, ref):
196         write_string(self.__top_ref_file, ref)
197
198     def __update_log_ref(self, ref):
199         write_string(self.__log_ref_file, ref)
200
201     def update_top_ref(self):
202         top = self.get_top()
203         if top:
204             self.__update_top_ref(top)
205
206     def get_old_bottom(self):
207         return self._get_field('bottom.old')
208
209     def get_bottom(self):
210         return self._get_field('bottom')
211
212     def set_bottom(self, value, backup = False):
213         if backup:
214             curr = self._get_field('bottom')
215             self._set_field('bottom.old', curr)
216         self._set_field('bottom', value)
217
218     def get_old_top(self):
219         return self._get_field('top.old')
220
221     def get_top(self):
222         return self._get_field('top')
223
224     def set_top(self, value, backup = False):
225         if backup:
226             curr = self._get_field('top')
227             self._set_field('top.old', curr)
228         self._set_field('top', value)
229         self.__update_top_ref(value)
230
231     def restore_old_boundaries(self):
232         bottom = self._get_field('bottom.old')
233         top = self._get_field('top.old')
234
235         if top and bottom:
236             self._set_field('bottom', bottom)
237             self._set_field('top', top)
238             self.__update_top_ref(top)
239             return True
240         else:
241             return False
242
243     def get_description(self):
244         return self._get_field('description', True)
245
246     def set_description(self, line):
247         self._set_field('description', line, True)
248
249     def get_authname(self):
250         return self._get_field('authname')
251
252     def set_authname(self, name):
253         self._set_field('authname', name or git.author().name)
254
255     def get_authemail(self):
256         return self._get_field('authemail')
257
258     def set_authemail(self, email):
259         self._set_field('authemail', email or git.author().email)
260
261     def get_authdate(self):
262         return self._get_field('authdate')
263
264     def set_authdate(self, date):
265         self._set_field('authdate', date or git.author().date)
266
267     def get_commname(self):
268         return self._get_field('commname')
269
270     def set_commname(self, name):
271         self._set_field('commname', name or git.committer().name)
272
273     def get_commemail(self):
274         return self._get_field('commemail')
275
276     def set_commemail(self, email):
277         self._set_field('commemail', email or git.committer().email)
278
279     def get_log(self):
280         return self._get_field('log')
281
282     def set_log(self, value, backup = False):
283         self._set_field('log', value)
284         self.__update_log_ref(value)
285
286
287 class Series(StgitObject):
288     """Class including the operations on series
289     """
290     def __init__(self, name = None):
291         """Takes a series name as the parameter.
292         """
293         try:
294             if name:
295                 self.__name = name
296             else:
297                 self.__name = git.get_head_file()
298             self.__base_dir = basedir.get()
299         except git.GitException, ex:
300             raise StackException, 'GIT tree not initialised: %s' % ex
301
302         self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
303         self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
304                                        self.__name)
305         self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
306                                         self.__name)
307
308         self.__applied_file = os.path.join(self._dir(), 'applied')
309         self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
310         self.__hidden_file = os.path.join(self._dir(), 'hidden')
311         self.__current_file = os.path.join(self._dir(), 'current')
312         self.__descr_file = os.path.join(self._dir(), 'description')
313
314         # where this series keeps its patches
315         self.__patch_dir = os.path.join(self._dir(), 'patches')
316         if not os.path.isdir(self.__patch_dir):
317             self.__patch_dir = self._dir()
318
319         # if no __refs_dir, create and populate it (upgrade old repositories)
320         if self.is_initialised() and not os.path.isdir(self.__refs_dir):
321             os.makedirs(self.__refs_dir)
322             for patch in self.get_applied() + self.get_unapplied():
323                 self.get_patch(patch).update_top_ref()
324
325         # trash directory
326         self.__trash_dir = os.path.join(self._dir(), 'trash')
327         if self.is_initialised() and not os.path.isdir(self.__trash_dir):
328             os.makedirs(self.__trash_dir)
329
330     def get_branch(self):
331         """Return the branch name for the Series object
332         """
333         return self.__name
334
335     def __set_current(self, name):
336         """Sets the topmost patch
337         """
338         self._set_field('current', name)
339
340     def get_patch(self, name):
341         """Return a Patch object for the given name
342         """
343         return Patch(name, self.__patch_dir, self.__refs_dir)
344
345     def get_current_patch(self):
346         """Return a Patch object representing the topmost patch, or
347         None if there is no such patch."""
348         crt = self.get_current()
349         if not crt:
350             return None
351         return Patch(crt, self.__patch_dir, self.__refs_dir)
352
353     def get_current(self):
354         """Return the name of the topmost patch, or None if there is
355         no such patch."""
356         name = self._get_field('current')
357         if name == '':
358             return None
359         else:
360             return name
361
362     def get_applied(self):
363         if not os.path.isfile(self.__applied_file):
364             raise StackException, 'Branch "%s" not initialised' % self.__name
365         f = file(self.__applied_file)
366         names = [line.strip() for line in f.readlines()]
367         f.close()
368         return names
369
370     def get_unapplied(self):
371         if not os.path.isfile(self.__unapplied_file):
372             raise StackException, 'Branch "%s" not initialised' % self.__name
373         f = file(self.__unapplied_file)
374         names = [line.strip() for line in f.readlines()]
375         f.close()
376         return names
377
378     def get_hidden(self):
379         if not os.path.isfile(self.__hidden_file):
380             return []
381         f = file(self.__hidden_file)
382         names = [line.strip() for line in f.readlines()]
383         f.close()
384         return names
385
386     def get_base_file(self):
387         self.__begin_stack_check()
388         return self.__base_file
389
390     def get_protected(self):
391         return os.path.isfile(os.path.join(self._dir(), 'protected'))
392
393     def protect(self):
394         protect_file = os.path.join(self._dir(), 'protected')
395         if not os.path.isfile(protect_file):
396             create_empty_file(protect_file)
397
398     def unprotect(self):
399         protect_file = os.path.join(self._dir(), 'protected')
400         if os.path.isfile(protect_file):
401             os.remove(protect_file)
402
403     def get_description(self):
404         return self._get_field('description') or ''
405
406     def set_description(self, line):
407         self._set_field('description', line)
408
409     def __patch_is_current(self, patch):
410         return patch.get_name() == self.get_current()
411
412     def patch_applied(self, name):
413         """Return true if the patch exists in the applied list
414         """
415         return name in self.get_applied()
416
417     def patch_unapplied(self, name):
418         """Return true if the patch exists in the unapplied list
419         """
420         return name in self.get_unapplied()
421
422     def patch_hidden(self, name):
423         """Return true if the patch is hidden.
424         """
425         return name in self.get_hidden()
426
427     def patch_exists(self, name):
428         """Return true if there is a patch with the given name, false
429         otherwise."""
430         return self.patch_applied(name) or self.patch_unapplied(name)
431
432     def __begin_stack_check(self):
433         """Save the current HEAD into .git/refs/heads/base if the stack
434         is empty
435         """
436         if len(self.get_applied()) == 0:
437             head = git.get_head()
438             write_string(self.__base_file, head)
439
440     def __end_stack_check(self):
441         """Remove .git/refs/heads/base if the stack is empty.
442         This warning should never happen
443         """
444         if len(self.get_applied()) == 0 \
445            and read_string(self.__base_file) != git.get_head():
446             print 'Warning: stack empty but the HEAD and base are different'
447
448     def head_top_equal(self):
449         """Return true if the head and the top are the same
450         """
451         crt = self.get_current_patch()
452         if not crt:
453             # we don't care, no patches applied
454             return True
455         return git.get_head() == crt.get_top()
456
457     def is_initialised(self):
458         """Checks if series is already initialised
459         """
460         return os.path.isdir(self.__patch_dir)
461
462     def init(self, create_at=False):
463         """Initialises the stgit series
464         """
465         bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
466
467         if os.path.exists(self.__patch_dir):
468             raise StackException, self.__patch_dir + ' already exists'
469         if os.path.exists(self.__refs_dir):
470             raise StackException, self.__refs_dir + ' already exists'
471         if os.path.exists(self.__base_file):
472             raise StackException, self.__base_file + ' already exists'
473
474         if (create_at!=False):
475             git.create_branch(self.__name, create_at)
476
477         os.makedirs(self.__patch_dir)
478
479         create_dirs(bases_dir)
480
481         self.create_empty_field('applied')
482         self.create_empty_field('unapplied')
483         self.create_empty_field('description')
484         os.makedirs(os.path.join(self._dir(), 'patches'))
485         os.makedirs(self.__refs_dir)
486         self.__begin_stack_check()
487
488     def convert(self):
489         """Either convert to use a separate patch directory, or
490         unconvert to place the patches in the same directory with
491         series control files
492         """
493         if self.__patch_dir == self._dir():
494             print 'Converting old-style to new-style...',
495             sys.stdout.flush()
496
497             self.__patch_dir = os.path.join(self._dir(), 'patches')
498             os.makedirs(self.__patch_dir)
499
500             for p in self.get_applied() + self.get_unapplied():
501                 src = os.path.join(self._dir(), p)
502                 dest = os.path.join(self.__patch_dir, p)
503                 os.rename(src, dest)
504
505             print 'done'
506
507         else:
508             print 'Converting new-style to old-style...',
509             sys.stdout.flush()
510
511             for p in self.get_applied() + self.get_unapplied():
512                 src = os.path.join(self.__patch_dir, p)
513                 dest = os.path.join(self._dir(), p)
514                 os.rename(src, dest)
515
516             if not os.listdir(self.__patch_dir):
517                 os.rmdir(self.__patch_dir)
518                 print 'done'
519             else:
520                 print 'Patch directory %s is not empty.' % self.__name
521
522             self.__patch_dir = self._dir()
523
524     def rename(self, to_name):
525         """Renames a series
526         """
527         to_stack = Series(to_name)
528
529         if to_stack.is_initialised():
530             raise StackException, '"%s" already exists' % to_stack.get_branch()
531         if os.path.exists(to_stack.__base_file):
532             os.remove(to_stack.__base_file)
533
534         git.rename_branch(self.__name, to_name)
535
536         if os.path.isdir(self._dir()):
537             rename(os.path.join(self.__base_dir, 'patches'),
538                    self.__name, to_stack.__name)
539         if os.path.exists(self.__base_file):
540             rename(os.path.join(self.__base_dir, 'refs', 'bases'),
541                    self.__name, to_stack.__name)
542         if os.path.exists(self.__refs_dir):
543             rename(os.path.join(self.__base_dir, 'refs', 'patches'),
544                    self.__name, to_stack.__name)
545
546         self.__init__(to_name)
547
548     def clone(self, target_series):
549         """Clones a series
550         """
551         try:
552             # allow cloning of branches not under StGIT control
553             base = read_string(self.get_base_file())
554         except:
555             base = git.get_head()
556         Series(target_series).init(create_at = base)
557         new_series = Series(target_series)
558
559         # generate an artificial description file
560         new_series.set_description('clone of "%s"' % self.__name)
561
562         # clone self's entire series as unapplied patches
563         try:
564             # allow cloning of branches not under StGIT control
565             applied = self.get_applied()
566             unapplied = self.get_unapplied()
567             patches = applied + unapplied
568             patches.reverse()
569         except:
570             patches = applied = unapplied = []
571         for p in patches:
572             patch = self.get_patch(p)
573             new_series.new_patch(p, message = patch.get_description(),
574                                  can_edit = False, unapplied = True,
575                                  bottom = patch.get_bottom(),
576                                  top = patch.get_top(),
577                                  author_name = patch.get_authname(),
578                                  author_email = patch.get_authemail(),
579                                  author_date = patch.get_authdate())
580
581         # fast forward the cloned series to self's top
582         new_series.forward_patches(applied)
583
584     def delete(self, force = False):
585         """Deletes an stgit series
586         """
587         if self.is_initialised():
588             patches = self.get_unapplied() + self.get_applied()
589             if not force and patches:
590                 raise StackException, \
591                       'Cannot delete: the series still contains patches'
592             for p in patches:
593                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
594
595             # remove the trash directory
596             for fname in os.listdir(self.__trash_dir):
597                 os.remove(fname)
598             os.rmdir(self.__trash_dir)
599
600             # FIXME: find a way to get rid of those manual removals
601             # (move functionnality to StgitObject ?)
602             if os.path.exists(self.__applied_file):
603                 os.remove(self.__applied_file)
604             if os.path.exists(self.__unapplied_file):
605                 os.remove(self.__unapplied_file)
606             if os.path.exists(self.__hidden_file):
607                 os.remove(self.__hidden_file)
608             if os.path.exists(self.__current_file):
609                 os.remove(self.__current_file)
610             if os.path.exists(self.__descr_file):
611                 os.remove(self.__descr_file)
612             if not os.listdir(self.__patch_dir):
613                 os.rmdir(self.__patch_dir)
614             else:
615                 print 'Patch directory %s is not empty.' % self.__name
616             if not os.listdir(self._dir()):
617                 remove_dirs(os.path.join(self.__base_dir, 'patches'),
618                             self.__name)
619             else:
620                 print 'Series directory %s is not empty.' % self.__name
621             if not os.listdir(self.__refs_dir):
622                 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
623                             self.__name)
624             else:
625                 print 'Refs directory %s is not empty.' % self.__refs_dir
626
627         if os.path.exists(self.__base_file):
628             remove_file_and_dirs(
629                 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
630
631     def refresh_patch(self, files = None, message = None, edit = False,
632                       show_patch = False,
633                       cache_update = True,
634                       author_name = None, author_email = None,
635                       author_date = None,
636                       committer_name = None, committer_email = None,
637                       backup = False, sign_str = None, log = 'refresh'):
638         """Generates a new commit for the given patch
639         """
640         name = self.get_current()
641         if not name:
642             raise StackException, 'No patches applied'
643
644         patch = Patch(name, self.__patch_dir, self.__refs_dir)
645
646         descr = patch.get_description()
647         if not (message or descr):
648             edit = True
649             descr = ''
650         elif message:
651             descr = message
652
653         if not message and edit:
654             descr = edit_file(self, descr.rstrip(), \
655                               'Please edit the description for patch "%s" ' \
656                               'above.' % name, show_patch)
657
658         if not author_name:
659             author_name = patch.get_authname()
660         if not author_email:
661             author_email = patch.get_authemail()
662         if not author_date:
663             author_date = patch.get_authdate()
664         if not committer_name:
665             committer_name = patch.get_commname()
666         if not committer_email:
667             committer_email = patch.get_commemail()
668
669         if sign_str:
670             descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
671                                            committer_name, committer_email)
672
673         bottom = patch.get_bottom()
674
675         commit_id = git.commit(files = files,
676                                message = descr, parents = [bottom],
677                                cache_update = cache_update,
678                                allowempty = True,
679                                author_name = author_name,
680                                author_email = author_email,
681                                author_date = author_date,
682                                committer_name = committer_name,
683                                committer_email = committer_email)
684
685         patch.set_bottom(bottom, backup = backup)
686         patch.set_top(commit_id, backup = backup)
687         patch.set_description(descr)
688         patch.set_authname(author_name)
689         patch.set_authemail(author_email)
690         patch.set_authdate(author_date)
691         patch.set_commname(committer_name)
692         patch.set_commemail(committer_email)
693
694         if log:
695             self.log_patch(patch, log)
696
697         return commit_id
698
699     def undo_refresh(self):
700         """Undo the patch boundaries changes caused by 'refresh'
701         """
702         name = self.get_current()
703         assert(name)
704
705         patch = Patch(name, self.__patch_dir, self.__refs_dir)
706         old_bottom = patch.get_old_bottom()
707         old_top = patch.get_old_top()
708
709         # the bottom of the patch is not changed by refresh. If the
710         # old_bottom is different, there wasn't any previous 'refresh'
711         # command (probably only a 'push')
712         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
713             raise StackException, 'No undo information available'
714
715         git.reset(tree_id = old_top, check_out = False)
716         if patch.restore_old_boundaries():
717             self.log_patch(patch, 'undo')
718
719     def new_patch(self, name, message = None, can_edit = True,
720                   unapplied = False, show_patch = False,
721                   top = None, bottom = None,
722                   author_name = None, author_email = None, author_date = None,
723                   committer_name = None, committer_email = None,
724                   before_existing = False, refresh = True):
725         """Creates a new patch
726         """
727         if self.patch_applied(name) or self.patch_unapplied(name):
728             raise StackException, 'Patch "%s" already exists' % name
729
730         if not message and can_edit:
731             descr = edit_file(self, None, \
732                               'Please enter the description for patch "%s" ' \
733                               'above.' % name, show_patch)
734         else:
735             descr = message
736
737         head = git.get_head()
738
739         self.__begin_stack_check()
740
741         patch = Patch(name, self.__patch_dir, self.__refs_dir)
742         patch.create()
743
744         if bottom:
745             patch.set_bottom(bottom)
746         else:
747             patch.set_bottom(head)
748         if top:
749             patch.set_top(top)
750         else:
751             patch.set_top(head)
752
753         patch.set_description(descr)
754         patch.set_authname(author_name)
755         patch.set_authemail(author_email)
756         patch.set_authdate(author_date)
757         patch.set_commname(committer_name)
758         patch.set_commemail(committer_email)
759
760         if unapplied:
761             self.log_patch(patch, 'new')
762
763             patches = [patch.get_name()] + self.get_unapplied()
764
765             f = file(self.__unapplied_file, 'w+')
766             f.writelines([line + '\n' for line in patches])
767             f.close()
768         elif before_existing:
769             self.log_patch(patch, 'new')
770
771             insert_string(self.__applied_file, patch.get_name())
772             if not self.get_current():
773                 self.__set_current(name)
774         else:
775             append_string(self.__applied_file, patch.get_name())
776             self.__set_current(name)
777             if refresh:
778                 self.refresh_patch(cache_update = False, log = 'new')
779
780     def delete_patch(self, name):
781         """Deletes a patch
782         """
783         patch = Patch(name, self.__patch_dir, self.__refs_dir)
784
785         if self.__patch_is_current(patch):
786             self.pop_patch(name)
787         elif self.patch_applied(name):
788             raise StackException, 'Cannot remove an applied patch, "%s", ' \
789                   'which is not current' % name
790         elif not name in self.get_unapplied():
791             raise StackException, 'Unknown patch "%s"' % name
792
793         # save the commit id to a trash file
794         write_string(os.path.join(self.__trash_dir, name), patch.get_top())
795
796         patch.delete()
797
798         unapplied = self.get_unapplied()
799         unapplied.remove(name)
800         f = file(self.__unapplied_file, 'w+')
801         f.writelines([line + '\n' for line in unapplied])
802         f.close()
803
804         if self.patch_hidden(name):
805             self.unhide_patch(name)
806
807         self.__begin_stack_check()
808
809     def forward_patches(self, names):
810         """Try to fast-forward an array of patches.
811
812         On return, patches in names[0:returned_value] have been pushed on the
813         stack. Apply the rest with push_patch
814         """
815         unapplied = self.get_unapplied()
816         self.__begin_stack_check()
817
818         forwarded = 0
819         top = git.get_head()
820
821         for name in names:
822             assert(name in unapplied)
823
824             patch = Patch(name, self.__patch_dir, self.__refs_dir)
825
826             head = top
827             bottom = patch.get_bottom()
828             top = patch.get_top()
829
830             # top != bottom always since we have a commit for each patch
831             if head == bottom:
832                 # reset the backup information. No logging since the
833                 # patch hasn't changed
834                 patch.set_bottom(head, backup = True)
835                 patch.set_top(top, backup = True)
836
837             else:
838                 head_tree = git.get_commit(head).get_tree()
839                 bottom_tree = git.get_commit(bottom).get_tree()
840                 if head_tree == bottom_tree:
841                     # We must just reparent this patch and create a new commit
842                     # for it
843                     descr = patch.get_description()
844                     author_name = patch.get_authname()
845                     author_email = patch.get_authemail()
846                     author_date = patch.get_authdate()
847                     committer_name = patch.get_commname()
848                     committer_email = patch.get_commemail()
849
850                     top_tree = git.get_commit(top).get_tree()
851
852                     top = git.commit(message = descr, parents = [head],
853                                      cache_update = False,
854                                      tree_id = top_tree,
855                                      allowempty = True,
856                                      author_name = author_name,
857                                      author_email = author_email,
858                                      author_date = author_date,
859                                      committer_name = committer_name,
860                                      committer_email = committer_email)
861
862                     patch.set_bottom(head, backup = True)
863                     patch.set_top(top, backup = True)
864
865                     self.log_patch(patch, 'push(f)')
866                 else:
867                     top = head
868                     # stop the fast-forwarding, must do a real merge
869                     break
870
871             forwarded+=1
872             unapplied.remove(name)
873
874         if forwarded == 0:
875             return 0
876
877         git.switch(top)
878
879         append_strings(self.__applied_file, names[0:forwarded])
880
881         f = file(self.__unapplied_file, 'w+')
882         f.writelines([line + '\n' for line in unapplied])
883         f.close()
884
885         self.__set_current(name)
886
887         return forwarded
888
889     def merged_patches(self, names):
890         """Test which patches were merged upstream by reverse-applying
891         them in reverse order. The function returns the list of
892         patches detected to have been applied. The state of the tree
893         is restored to the original one
894         """
895         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
896                    for name in names]
897         patches.reverse()
898
899         merged = []
900         for p in patches:
901             if git.apply_diff(p.get_top(), p.get_bottom()):
902                 merged.append(p.get_name())
903         merged.reverse()
904
905         git.reset()
906
907         return merged
908
909     def push_patch(self, name, empty = False):
910         """Pushes a patch on the stack
911         """
912         unapplied = self.get_unapplied()
913         assert(name in unapplied)
914
915         self.__begin_stack_check()
916
917         patch = Patch(name, self.__patch_dir, self.__refs_dir)
918
919         head = git.get_head()
920         bottom = patch.get_bottom()
921         top = patch.get_top()
922
923         ex = None
924         modified = False
925
926         # top != bottom always since we have a commit for each patch
927         if empty:
928             # just make an empty patch (top = bottom = HEAD). This
929             # option is useful to allow undoing already merged
930             # patches. The top is updated by refresh_patch since we
931             # need an empty commit
932             patch.set_bottom(head, backup = True)
933             patch.set_top(head, backup = True)
934             modified = True
935         elif head == bottom:
936             # reset the backup information. No need for logging
937             patch.set_bottom(bottom, backup = True)
938             patch.set_top(top, backup = True)
939
940             git.switch(top)
941         else:
942             # new patch needs to be refreshed.
943             # The current patch is empty after merge.
944             patch.set_bottom(head, backup = True)
945             patch.set_top(head, backup = True)
946
947             # Try the fast applying first. If this fails, fall back to the
948             # three-way merge
949             if not git.apply_diff(bottom, top):
950                 # if git.apply_diff() fails, the patch requires a diff3
951                 # merge and can be reported as modified
952                 modified = True
953
954                 # merge can fail but the patch needs to be pushed
955                 try:
956                     git.merge(bottom, head, top, recursive = True)
957                 except git.GitException, ex:
958                     print >> sys.stderr, \
959                           'The merge failed during "push". ' \
960                           'Use "refresh" after fixing the conflicts'
961
962         append_string(self.__applied_file, name)
963
964         unapplied.remove(name)
965         f = file(self.__unapplied_file, 'w+')
966         f.writelines([line + '\n' for line in unapplied])
967         f.close()
968
969         self.__set_current(name)
970
971         # head == bottom case doesn't need to refresh the patch
972         if empty or head != bottom:
973             if not ex:
974                 # if the merge was OK and no conflicts, just refresh the patch
975                 # The GIT cache was already updated by the merge operation
976                 if modified:
977                     log = 'push(m)'
978                 else:
979                     log = 'push'
980                 self.refresh_patch(cache_update = False, log = log)
981             else:
982                 # we store the correctly merged files only for
983                 # tracking the conflict history. Note that the
984                 # git.merge() operations shouls always leave the index
985                 # in a valid state (i.e. only stage 0 files)
986                 self.refresh_patch(cache_update = False, log = 'push(c)')
987                 raise StackException, str(ex)
988
989         return modified
990
991     def undo_push(self):
992         name = self.get_current()
993         assert(name)
994
995         patch = Patch(name, self.__patch_dir, self.__refs_dir)
996         old_bottom = patch.get_old_bottom()
997         old_top = patch.get_old_top()
998
999         # the top of the patch is changed by a push operation only
1000         # together with the bottom (otherwise the top was probably
1001         # modified by 'refresh'). If they are both unchanged, there
1002         # was a fast forward
1003         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1004             raise StackException, 'No undo information available'
1005
1006         git.reset()
1007         self.pop_patch(name)
1008         ret = patch.restore_old_boundaries()
1009         if ret:
1010             self.log_patch(patch, 'undo')
1011
1012         return ret
1013
1014     def pop_patch(self, name, keep = False):
1015         """Pops the top patch from the stack
1016         """
1017         applied = self.get_applied()
1018         applied.reverse()
1019         assert(name in applied)
1020
1021         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1022
1023         # only keep the local changes
1024         if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1025             raise StackException, \
1026                   'Failed to pop patches while preserving the local changes'
1027
1028         git.switch(patch.get_bottom(), keep)
1029
1030         # save the new applied list
1031         idx = applied.index(name) + 1
1032
1033         popped = applied[:idx]
1034         popped.reverse()
1035         unapplied = popped + self.get_unapplied()
1036
1037         f = file(self.__unapplied_file, 'w+')
1038         f.writelines([line + '\n' for line in unapplied])
1039         f.close()
1040
1041         del applied[:idx]
1042         applied.reverse()
1043
1044         f = file(self.__applied_file, 'w+')
1045         f.writelines([line + '\n' for line in applied])
1046         f.close()
1047
1048         if applied == []:
1049             self.__set_current(None)
1050         else:
1051             self.__set_current(applied[-1])
1052
1053         self.__end_stack_check()
1054
1055     def empty_patch(self, name):
1056         """Returns True if the patch is empty
1057         """
1058         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1059         bottom = patch.get_bottom()
1060         top = patch.get_top()
1061
1062         if bottom == top:
1063             return True
1064         elif git.get_commit(top).get_tree() \
1065                  == git.get_commit(bottom).get_tree():
1066             return True
1067
1068         return False
1069
1070     def rename_patch(self, oldname, newname):
1071         applied = self.get_applied()
1072         unapplied = self.get_unapplied()
1073
1074         if oldname == newname:
1075             raise StackException, '"To" name and "from" name are the same'
1076
1077         if newname in applied or newname in unapplied:
1078             raise StackException, 'Patch "%s" already exists' % newname
1079
1080         if self.patch_hidden(oldname):
1081             self.unhide_patch(oldname)
1082             self.hide_patch(newname)
1083
1084         if oldname in unapplied:
1085             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1086             unapplied[unapplied.index(oldname)] = newname
1087
1088             f = file(self.__unapplied_file, 'w+')
1089             f.writelines([line + '\n' for line in unapplied])
1090             f.close()
1091         elif oldname in applied:
1092             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1093             if oldname == self.get_current():
1094                 self.__set_current(newname)
1095
1096             applied[applied.index(oldname)] = newname
1097
1098             f = file(self.__applied_file, 'w+')
1099             f.writelines([line + '\n' for line in applied])
1100             f.close()
1101         else:
1102             raise StackException, 'Unknown patch "%s"' % oldname
1103
1104     def log_patch(self, patch, message):
1105         """Generate a log commit for a patch
1106         """
1107         top = git.get_commit(patch.get_top())
1108         msg = '%s\t%s' % (message, top.get_id_hash())
1109
1110         old_log = patch.get_log()
1111         if old_log:
1112             parents = [old_log]
1113         else:
1114             parents = []
1115
1116         log = git.commit(message = msg, parents = parents,
1117                          cache_update = False, tree_id = top.get_tree(),
1118                          allowempty = True)
1119         patch.set_log(log)
1120
1121     def hide_patch(self, name):
1122         """Add the patch to the hidden list.
1123         """
1124         if not self.patch_exists(name):
1125             raise StackException, 'Unknown patch "%s"' % name
1126         elif self.patch_hidden(name):
1127             raise StackException, 'Patch "%s" already hidden' % name
1128
1129         append_string(self.__hidden_file, name)
1130
1131     def unhide_patch(self, name):
1132         """Add the patch to the hidden list.
1133         """
1134         if not self.patch_exists(name):
1135             raise StackException, 'Unknown patch "%s"' % name
1136         hidden = self.get_hidden()
1137         if not name in hidden:
1138             raise StackException, 'Patch "%s" not hidden' % name
1139
1140         hidden.remove(name)
1141
1142         f = file(self.__hidden_file, 'w+')
1143         f.writelines([line + '\n' for line in hidden])
1144         f.close()