chiark / gitweb /
3960729c8abf4c9a4738c2b6b879a0d6f9e66fb7
[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, re
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 __patch_name_valid(self, name):
332         """Raise an exception if the patch name is not valid.
333         """
334         if not name or re.search('[^\w.-]', name):
335             raise StackException, 'Invalid patch name: "%s"' % name
336
337     def get_branch(self):
338         """Return the branch name for the Series object
339         """
340         return self.__name
341
342     def __set_current(self, name):
343         """Sets the topmost patch
344         """
345         self._set_field('current', name)
346
347     def get_patch(self, name):
348         """Return a Patch object for the given name
349         """
350         return Patch(name, self.__patch_dir, self.__refs_dir)
351
352     def get_current_patch(self):
353         """Return a Patch object representing the topmost patch, or
354         None if there is no such patch."""
355         crt = self.get_current()
356         if not crt:
357             return None
358         return Patch(crt, self.__patch_dir, self.__refs_dir)
359
360     def get_current(self):
361         """Return the name of the topmost patch, or None if there is
362         no such patch."""
363         name = self._get_field('current')
364         if name == '':
365             return None
366         else:
367             return name
368
369     def get_applied(self):
370         if not os.path.isfile(self.__applied_file):
371             raise StackException, 'Branch "%s" not initialised' % self.__name
372         f = file(self.__applied_file)
373         names = [line.strip() for line in f.readlines()]
374         f.close()
375         return names
376
377     def get_unapplied(self):
378         if not os.path.isfile(self.__unapplied_file):
379             raise StackException, 'Branch "%s" not initialised' % self.__name
380         f = file(self.__unapplied_file)
381         names = [line.strip() for line in f.readlines()]
382         f.close()
383         return names
384
385     def get_hidden(self):
386         if not os.path.isfile(self.__hidden_file):
387             return []
388         f = file(self.__hidden_file)
389         names = [line.strip() for line in f.readlines()]
390         f.close()
391         return names
392
393     def get_base_file(self):
394         self.__begin_stack_check()
395         return self.__base_file
396
397     def get_protected(self):
398         return os.path.isfile(os.path.join(self._dir(), 'protected'))
399
400     def protect(self):
401         protect_file = os.path.join(self._dir(), 'protected')
402         if not os.path.isfile(protect_file):
403             create_empty_file(protect_file)
404
405     def unprotect(self):
406         protect_file = os.path.join(self._dir(), 'protected')
407         if os.path.isfile(protect_file):
408             os.remove(protect_file)
409
410     def get_description(self):
411         return self._get_field('description') or ''
412
413     def set_description(self, line):
414         self._set_field('description', line)
415
416     def get_parent_remote(self):
417         value = config.get('branch.%s.remote' % self.__name)
418         if value:
419             return value
420         elif 'origin' in git.remotes_list():
421             # FIXME: this is for compatibility only.  Should be
422             # dropped when all relevant commands record this info.
423             return 'origin'
424         else:
425             raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
426
427     def __set_parent_remote(self, remote):
428         value = config.set('branch.%s.remote' % self.__name, remote)
429
430     def get_parent_branch(self):
431         value = config.get('branch.%s.merge' % self.__name)
432         if value:
433             return value
434         elif git.rev_parse('heads/origin'):
435             # FIXME: this is for compatibility only.  Should be
436             # dropped when all relevant commands record this info.
437             return 'heads/origin'
438         else:
439             raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
440
441     def __set_parent_branch(self, name):
442         config.set('branch.%s.merge' % self.__name, name)
443
444     def set_parent(self, remote, localbranch):
445         if localbranch:
446             self.__set_parent_branch(localbranch)
447             if remote:
448                 self.__set_parent_remote(remote)
449         elif remote:
450             raise StackException, 'Remote "%s" without a branch cannot be used as parent' % remote
451
452     def __patch_is_current(self, patch):
453         return patch.get_name() == self.get_current()
454
455     def patch_applied(self, name):
456         """Return true if the patch exists in the applied list
457         """
458         return name in self.get_applied()
459
460     def patch_unapplied(self, name):
461         """Return true if the patch exists in the unapplied list
462         """
463         return name in self.get_unapplied()
464
465     def patch_hidden(self, name):
466         """Return true if the patch is hidden.
467         """
468         return name in self.get_hidden()
469
470     def patch_exists(self, name):
471         """Return true if there is a patch with the given name, false
472         otherwise."""
473         return self.patch_applied(name) or self.patch_unapplied(name)
474
475     def __begin_stack_check(self):
476         """Save the current HEAD into .git/refs/heads/base if the stack
477         is empty
478         """
479         if len(self.get_applied()) == 0:
480             head = git.get_head()
481             write_string(self.__base_file, head)
482
483     def __end_stack_check(self):
484         """Remove .git/refs/heads/base if the stack is empty.
485         This warning should never happen
486         """
487         if len(self.get_applied()) == 0 \
488            and read_string(self.__base_file) != git.get_head():
489             print 'Warning: stack empty but the HEAD and base are different'
490
491     def head_top_equal(self):
492         """Return true if the head and the top are the same
493         """
494         crt = self.get_current_patch()
495         if not crt:
496             # we don't care, no patches applied
497             return True
498         return git.get_head() == crt.get_top()
499
500     def is_initialised(self):
501         """Checks if series is already initialised
502         """
503         return os.path.isdir(self.__patch_dir)
504
505     def init(self, create_at=False, parent_remote=None, parent_branch=None):
506         """Initialises the stgit series
507         """
508         bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
509
510         if os.path.exists(self.__patch_dir):
511             raise StackException, self.__patch_dir + ' already exists'
512         if os.path.exists(self.__refs_dir):
513             raise StackException, self.__refs_dir + ' already exists'
514         if os.path.exists(self.__base_file):
515             raise StackException, self.__base_file + ' already exists'
516
517         if (create_at!=False):
518             git.create_branch(self.__name, create_at)
519
520         os.makedirs(self.__patch_dir)
521
522         self.set_parent(parent_remote, parent_branch)
523         
524         create_dirs(bases_dir)
525
526         self.create_empty_field('applied')
527         self.create_empty_field('unapplied')
528         self.create_empty_field('description')
529         os.makedirs(os.path.join(self._dir(), 'patches'))
530         os.makedirs(self.__refs_dir)
531         self.__begin_stack_check()
532
533     def convert(self):
534         """Either convert to use a separate patch directory, or
535         unconvert to place the patches in the same directory with
536         series control files
537         """
538         if self.__patch_dir == self._dir():
539             print 'Converting old-style to new-style...',
540             sys.stdout.flush()
541
542             self.__patch_dir = os.path.join(self._dir(), 'patches')
543             os.makedirs(self.__patch_dir)
544
545             for p in self.get_applied() + self.get_unapplied():
546                 src = os.path.join(self._dir(), p)
547                 dest = os.path.join(self.__patch_dir, p)
548                 os.rename(src, dest)
549
550             print 'done'
551
552         else:
553             print 'Converting new-style to old-style...',
554             sys.stdout.flush()
555
556             for p in self.get_applied() + self.get_unapplied():
557                 src = os.path.join(self.__patch_dir, p)
558                 dest = os.path.join(self._dir(), p)
559                 os.rename(src, dest)
560
561             if not os.listdir(self.__patch_dir):
562                 os.rmdir(self.__patch_dir)
563                 print 'done'
564             else:
565                 print 'Patch directory %s is not empty.' % self.__name
566
567             self.__patch_dir = self._dir()
568
569     def rename(self, to_name):
570         """Renames a series
571         """
572         to_stack = Series(to_name)
573
574         if to_stack.is_initialised():
575             raise StackException, '"%s" already exists' % to_stack.get_branch()
576         if os.path.exists(to_stack.__base_file):
577             os.remove(to_stack.__base_file)
578
579         git.rename_branch(self.__name, to_name)
580
581         if os.path.isdir(self._dir()):
582             rename(os.path.join(self.__base_dir, 'patches'),
583                    self.__name, to_stack.__name)
584         if os.path.exists(self.__base_file):
585             rename(os.path.join(self.__base_dir, 'refs', 'bases'),
586                    self.__name, to_stack.__name)
587         if os.path.exists(self.__refs_dir):
588             rename(os.path.join(self.__base_dir, 'refs', 'patches'),
589                    self.__name, to_stack.__name)
590
591         self.__init__(to_name)
592
593     def clone(self, target_series):
594         """Clones a series
595         """
596         try:
597             # allow cloning of branches not under StGIT control
598             base = read_string(self.get_base_file())
599         except:
600             base = git.get_head()
601         Series(target_series).init(create_at = base)
602         new_series = Series(target_series)
603
604         # generate an artificial description file
605         new_series.set_description('clone of "%s"' % self.__name)
606
607         # clone self's entire series as unapplied patches
608         try:
609             # allow cloning of branches not under StGIT control
610             applied = self.get_applied()
611             unapplied = self.get_unapplied()
612             patches = applied + unapplied
613             patches.reverse()
614         except:
615             patches = applied = unapplied = []
616         for p in patches:
617             patch = self.get_patch(p)
618             new_series.new_patch(p, message = patch.get_description(),
619                                  can_edit = False, unapplied = True,
620                                  bottom = patch.get_bottom(),
621                                  top = patch.get_top(),
622                                  author_name = patch.get_authname(),
623                                  author_email = patch.get_authemail(),
624                                  author_date = patch.get_authdate())
625
626         # fast forward the cloned series to self's top
627         new_series.forward_patches(applied)
628
629     def delete(self, force = False):
630         """Deletes an stgit series
631         """
632         if self.is_initialised():
633             patches = self.get_unapplied() + self.get_applied()
634             if not force and patches:
635                 raise StackException, \
636                       'Cannot delete: the series still contains patches'
637             for p in patches:
638                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
639
640             # remove the trash directory
641             for fname in os.listdir(self.__trash_dir):
642                 os.remove(fname)
643             os.rmdir(self.__trash_dir)
644
645             # FIXME: find a way to get rid of those manual removals
646             # (move functionnality to StgitObject ?)
647             if os.path.exists(self.__applied_file):
648                 os.remove(self.__applied_file)
649             if os.path.exists(self.__unapplied_file):
650                 os.remove(self.__unapplied_file)
651             if os.path.exists(self.__hidden_file):
652                 os.remove(self.__hidden_file)
653             if os.path.exists(self.__current_file):
654                 os.remove(self.__current_file)
655             if os.path.exists(self.__descr_file):
656                 os.remove(self.__descr_file)
657             if not os.listdir(self.__patch_dir):
658                 os.rmdir(self.__patch_dir)
659             else:
660                 print 'Patch directory %s is not empty.' % self.__name
661             if not os.listdir(self._dir()):
662                 remove_dirs(os.path.join(self.__base_dir, 'patches'),
663                             self.__name)
664             else:
665                 print 'Series directory %s is not empty.' % self.__name
666             if not os.listdir(self.__refs_dir):
667                 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
668                             self.__name)
669             else:
670                 print 'Refs directory %s is not empty.' % self.__refs_dir
671
672         if os.path.exists(self.__base_file):
673             remove_file_and_dirs(
674                 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
675
676     def refresh_patch(self, files = None, message = None, edit = False,
677                       show_patch = False,
678                       cache_update = True,
679                       author_name = None, author_email = None,
680                       author_date = None,
681                       committer_name = None, committer_email = None,
682                       backup = False, sign_str = None, log = 'refresh'):
683         """Generates a new commit for the given patch
684         """
685         name = self.get_current()
686         if not name:
687             raise StackException, 'No patches applied'
688
689         patch = Patch(name, self.__patch_dir, self.__refs_dir)
690
691         descr = patch.get_description()
692         if not (message or descr):
693             edit = True
694             descr = ''
695         elif message:
696             descr = message
697
698         if not message and edit:
699             descr = edit_file(self, descr.rstrip(), \
700                               'Please edit the description for patch "%s" ' \
701                               'above.' % name, show_patch)
702
703         if not author_name:
704             author_name = patch.get_authname()
705         if not author_email:
706             author_email = patch.get_authemail()
707         if not author_date:
708             author_date = patch.get_authdate()
709         if not committer_name:
710             committer_name = patch.get_commname()
711         if not committer_email:
712             committer_email = patch.get_commemail()
713
714         if sign_str:
715             descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
716                                            committer_name, committer_email)
717
718         bottom = patch.get_bottom()
719
720         commit_id = git.commit(files = files,
721                                message = descr, parents = [bottom],
722                                cache_update = cache_update,
723                                allowempty = True,
724                                author_name = author_name,
725                                author_email = author_email,
726                                author_date = author_date,
727                                committer_name = committer_name,
728                                committer_email = committer_email)
729
730         patch.set_bottom(bottom, backup = backup)
731         patch.set_top(commit_id, backup = backup)
732         patch.set_description(descr)
733         patch.set_authname(author_name)
734         patch.set_authemail(author_email)
735         patch.set_authdate(author_date)
736         patch.set_commname(committer_name)
737         patch.set_commemail(committer_email)
738
739         if log:
740             self.log_patch(patch, log)
741
742         return commit_id
743
744     def undo_refresh(self):
745         """Undo the patch boundaries changes caused by 'refresh'
746         """
747         name = self.get_current()
748         assert(name)
749
750         patch = Patch(name, self.__patch_dir, self.__refs_dir)
751         old_bottom = patch.get_old_bottom()
752         old_top = patch.get_old_top()
753
754         # the bottom of the patch is not changed by refresh. If the
755         # old_bottom is different, there wasn't any previous 'refresh'
756         # command (probably only a 'push')
757         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
758             raise StackException, 'No undo information available'
759
760         git.reset(tree_id = old_top, check_out = False)
761         if patch.restore_old_boundaries():
762             self.log_patch(patch, 'undo')
763
764     def new_patch(self, name, message = None, can_edit = True,
765                   unapplied = False, show_patch = False,
766                   top = None, bottom = None,
767                   author_name = None, author_email = None, author_date = None,
768                   committer_name = None, committer_email = None,
769                   before_existing = False, refresh = True):
770         """Creates a new patch
771         """
772         self.__patch_name_valid(name)
773
774         if self.patch_applied(name) or self.patch_unapplied(name):
775             raise StackException, 'Patch "%s" already exists' % name
776
777         if not message and can_edit:
778             descr = edit_file(self, None, \
779                               'Please enter the description for patch "%s" ' \
780                               'above.' % name, show_patch)
781         else:
782             descr = message
783
784         head = git.get_head()
785
786         self.__begin_stack_check()
787
788         patch = Patch(name, self.__patch_dir, self.__refs_dir)
789         patch.create()
790
791         if bottom:
792             patch.set_bottom(bottom)
793         else:
794             patch.set_bottom(head)
795         if top:
796             patch.set_top(top)
797         else:
798             patch.set_top(head)
799
800         patch.set_description(descr)
801         patch.set_authname(author_name)
802         patch.set_authemail(author_email)
803         patch.set_authdate(author_date)
804         patch.set_commname(committer_name)
805         patch.set_commemail(committer_email)
806
807         if unapplied:
808             self.log_patch(patch, 'new')
809
810             patches = [patch.get_name()] + self.get_unapplied()
811
812             f = file(self.__unapplied_file, 'w+')
813             f.writelines([line + '\n' for line in patches])
814             f.close()
815         elif before_existing:
816             self.log_patch(patch, 'new')
817
818             insert_string(self.__applied_file, patch.get_name())
819             if not self.get_current():
820                 self.__set_current(name)
821         else:
822             append_string(self.__applied_file, patch.get_name())
823             self.__set_current(name)
824             if refresh:
825                 self.refresh_patch(cache_update = False, log = 'new')
826
827     def delete_patch(self, name):
828         """Deletes a patch
829         """
830         self.__patch_name_valid(name)
831         patch = Patch(name, self.__patch_dir, self.__refs_dir)
832
833         if self.__patch_is_current(patch):
834             self.pop_patch(name)
835         elif self.patch_applied(name):
836             raise StackException, 'Cannot remove an applied patch, "%s", ' \
837                   'which is not current' % name
838         elif not name in self.get_unapplied():
839             raise StackException, 'Unknown patch "%s"' % name
840
841         # save the commit id to a trash file
842         write_string(os.path.join(self.__trash_dir, name), patch.get_top())
843
844         patch.delete()
845
846         unapplied = self.get_unapplied()
847         unapplied.remove(name)
848         f = file(self.__unapplied_file, 'w+')
849         f.writelines([line + '\n' for line in unapplied])
850         f.close()
851
852         if self.patch_hidden(name):
853             self.unhide_patch(name)
854
855         self.__begin_stack_check()
856
857     def forward_patches(self, names):
858         """Try to fast-forward an array of patches.
859
860         On return, patches in names[0:returned_value] have been pushed on the
861         stack. Apply the rest with push_patch
862         """
863         unapplied = self.get_unapplied()
864         self.__begin_stack_check()
865
866         forwarded = 0
867         top = git.get_head()
868
869         for name in names:
870             assert(name in unapplied)
871
872             patch = Patch(name, self.__patch_dir, self.__refs_dir)
873
874             head = top
875             bottom = patch.get_bottom()
876             top = patch.get_top()
877
878             # top != bottom always since we have a commit for each patch
879             if head == bottom:
880                 # reset the backup information. No logging since the
881                 # patch hasn't changed
882                 patch.set_bottom(head, backup = True)
883                 patch.set_top(top, backup = True)
884
885             else:
886                 head_tree = git.get_commit(head).get_tree()
887                 bottom_tree = git.get_commit(bottom).get_tree()
888                 if head_tree == bottom_tree:
889                     # We must just reparent this patch and create a new commit
890                     # for it
891                     descr = patch.get_description()
892                     author_name = patch.get_authname()
893                     author_email = patch.get_authemail()
894                     author_date = patch.get_authdate()
895                     committer_name = patch.get_commname()
896                     committer_email = patch.get_commemail()
897
898                     top_tree = git.get_commit(top).get_tree()
899
900                     top = git.commit(message = descr, parents = [head],
901                                      cache_update = False,
902                                      tree_id = top_tree,
903                                      allowempty = True,
904                                      author_name = author_name,
905                                      author_email = author_email,
906                                      author_date = author_date,
907                                      committer_name = committer_name,
908                                      committer_email = committer_email)
909
910                     patch.set_bottom(head, backup = True)
911                     patch.set_top(top, backup = True)
912
913                     self.log_patch(patch, 'push(f)')
914                 else:
915                     top = head
916                     # stop the fast-forwarding, must do a real merge
917                     break
918
919             forwarded+=1
920             unapplied.remove(name)
921
922         if forwarded == 0:
923             return 0
924
925         git.switch(top)
926
927         append_strings(self.__applied_file, names[0:forwarded])
928
929         f = file(self.__unapplied_file, 'w+')
930         f.writelines([line + '\n' for line in unapplied])
931         f.close()
932
933         self.__set_current(name)
934
935         return forwarded
936
937     def merged_patches(self, names):
938         """Test which patches were merged upstream by reverse-applying
939         them in reverse order. The function returns the list of
940         patches detected to have been applied. The state of the tree
941         is restored to the original one
942         """
943         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
944                    for name in names]
945         patches.reverse()
946
947         merged = []
948         for p in patches:
949             if git.apply_diff(p.get_top(), p.get_bottom()):
950                 merged.append(p.get_name())
951         merged.reverse()
952
953         git.reset()
954
955         return merged
956
957     def push_patch(self, name, empty = False):
958         """Pushes a patch on the stack
959         """
960         unapplied = self.get_unapplied()
961         assert(name in unapplied)
962
963         self.__begin_stack_check()
964
965         patch = Patch(name, self.__patch_dir, self.__refs_dir)
966
967         head = git.get_head()
968         bottom = patch.get_bottom()
969         top = patch.get_top()
970
971         ex = None
972         modified = False
973
974         # top != bottom always since we have a commit for each patch
975         if empty:
976             # just make an empty patch (top = bottom = HEAD). This
977             # option is useful to allow undoing already merged
978             # patches. The top is updated by refresh_patch since we
979             # need an empty commit
980             patch.set_bottom(head, backup = True)
981             patch.set_top(head, backup = True)
982             modified = True
983         elif head == bottom:
984             # reset the backup information. No need for logging
985             patch.set_bottom(bottom, backup = True)
986             patch.set_top(top, backup = True)
987
988             git.switch(top)
989         else:
990             # new patch needs to be refreshed.
991             # The current patch is empty after merge.
992             patch.set_bottom(head, backup = True)
993             patch.set_top(head, backup = True)
994
995             # Try the fast applying first. If this fails, fall back to the
996             # three-way merge
997             if not git.apply_diff(bottom, top):
998                 # if git.apply_diff() fails, the patch requires a diff3
999                 # merge and can be reported as modified
1000                 modified = True
1001
1002                 # merge can fail but the patch needs to be pushed
1003                 try:
1004                     git.merge(bottom, head, top, recursive = True)
1005                 except git.GitException, ex:
1006                     print >> sys.stderr, \
1007                           'The merge failed during "push". ' \
1008                           'Use "refresh" after fixing the conflicts'
1009
1010         append_string(self.__applied_file, name)
1011
1012         unapplied.remove(name)
1013         f = file(self.__unapplied_file, 'w+')
1014         f.writelines([line + '\n' for line in unapplied])
1015         f.close()
1016
1017         self.__set_current(name)
1018
1019         # head == bottom case doesn't need to refresh the patch
1020         if empty or head != bottom:
1021             if not ex:
1022                 # if the merge was OK and no conflicts, just refresh the patch
1023                 # The GIT cache was already updated by the merge operation
1024                 if modified:
1025                     log = 'push(m)'
1026                 else:
1027                     log = 'push'
1028                 self.refresh_patch(cache_update = False, log = log)
1029             else:
1030                 # we store the correctly merged files only for
1031                 # tracking the conflict history. Note that the
1032                 # git.merge() operations shouls always leave the index
1033                 # in a valid state (i.e. only stage 0 files)
1034                 self.refresh_patch(cache_update = False, log = 'push(c)')
1035                 raise StackException, str(ex)
1036
1037         return modified
1038
1039     def undo_push(self):
1040         name = self.get_current()
1041         assert(name)
1042
1043         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1044         old_bottom = patch.get_old_bottom()
1045         old_top = patch.get_old_top()
1046
1047         # the top of the patch is changed by a push operation only
1048         # together with the bottom (otherwise the top was probably
1049         # modified by 'refresh'). If they are both unchanged, there
1050         # was a fast forward
1051         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1052             raise StackException, 'No undo information available'
1053
1054         git.reset()
1055         self.pop_patch(name)
1056         ret = patch.restore_old_boundaries()
1057         if ret:
1058             self.log_patch(patch, 'undo')
1059
1060         return ret
1061
1062     def pop_patch(self, name, keep = False):
1063         """Pops the top patch from the stack
1064         """
1065         applied = self.get_applied()
1066         applied.reverse()
1067         assert(name in applied)
1068
1069         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1070
1071         # only keep the local changes
1072         if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1073             raise StackException, \
1074                   'Failed to pop patches while preserving the local changes'
1075
1076         git.switch(patch.get_bottom(), keep)
1077
1078         # save the new applied list
1079         idx = applied.index(name) + 1
1080
1081         popped = applied[:idx]
1082         popped.reverse()
1083         unapplied = popped + self.get_unapplied()
1084
1085         f = file(self.__unapplied_file, 'w+')
1086         f.writelines([line + '\n' for line in unapplied])
1087         f.close()
1088
1089         del applied[:idx]
1090         applied.reverse()
1091
1092         f = file(self.__applied_file, 'w+')
1093         f.writelines([line + '\n' for line in applied])
1094         f.close()
1095
1096         if applied == []:
1097             self.__set_current(None)
1098         else:
1099             self.__set_current(applied[-1])
1100
1101         self.__end_stack_check()
1102
1103     def empty_patch(self, name):
1104         """Returns True if the patch is empty
1105         """
1106         self.__patch_name_valid(name)
1107         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1108         bottom = patch.get_bottom()
1109         top = patch.get_top()
1110
1111         if bottom == top:
1112             return True
1113         elif git.get_commit(top).get_tree() \
1114                  == git.get_commit(bottom).get_tree():
1115             return True
1116
1117         return False
1118
1119     def rename_patch(self, oldname, newname):
1120         self.__patch_name_valid(newname)
1121
1122         applied = self.get_applied()
1123         unapplied = self.get_unapplied()
1124
1125         if oldname == newname:
1126             raise StackException, '"To" name and "from" name are the same'
1127
1128         if newname in applied or newname in unapplied:
1129             raise StackException, 'Patch "%s" already exists' % newname
1130
1131         if self.patch_hidden(oldname):
1132             self.unhide_patch(oldname)
1133             self.hide_patch(newname)
1134
1135         if oldname in unapplied:
1136             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1137             unapplied[unapplied.index(oldname)] = newname
1138
1139             f = file(self.__unapplied_file, 'w+')
1140             f.writelines([line + '\n' for line in unapplied])
1141             f.close()
1142         elif oldname in applied:
1143             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1144             if oldname == self.get_current():
1145                 self.__set_current(newname)
1146
1147             applied[applied.index(oldname)] = newname
1148
1149             f = file(self.__applied_file, 'w+')
1150             f.writelines([line + '\n' for line in applied])
1151             f.close()
1152         else:
1153             raise StackException, 'Unknown patch "%s"' % oldname
1154
1155     def log_patch(self, patch, message):
1156         """Generate a log commit for a patch
1157         """
1158         top = git.get_commit(patch.get_top())
1159         msg = '%s\t%s' % (message, top.get_id_hash())
1160
1161         old_log = patch.get_log()
1162         if old_log:
1163             parents = [old_log]
1164         else:
1165             parents = []
1166
1167         log = git.commit(message = msg, parents = parents,
1168                          cache_update = False, tree_id = top.get_tree(),
1169                          allowempty = True)
1170         patch.set_log(log)
1171
1172     def hide_patch(self, name):
1173         """Add the patch to the hidden list.
1174         """
1175         if not self.patch_exists(name):
1176             raise StackException, 'Unknown patch "%s"' % name
1177         elif self.patch_hidden(name):
1178             raise StackException, 'Patch "%s" already hidden' % name
1179
1180         append_string(self.__hidden_file, name)
1181
1182     def unhide_patch(self, name):
1183         """Add the patch to the hidden list.
1184         """
1185         if not self.patch_exists(name):
1186             raise StackException, 'Unknown patch "%s"' % name
1187         hidden = self.get_hidden()
1188         if not name in hidden:
1189             raise StackException, 'Patch "%s" not hidden' % name
1190
1191         hidden.remove(name)
1192
1193         f = file(self.__hidden_file, 'w+')
1194         f.writelines([line + '\n' for line in hidden])
1195         f.close()