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