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