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