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