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