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