chiark / gitweb /
d9c4b99728711210da38e8ec843bc6fc72f9fb7b
[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 from shutil import copyfile
27
28
29 # stack exception class
30 class StackException(Exception):
31     pass
32
33 class FilterUntil:
34     def __init__(self):
35         self.should_print = True
36     def __call__(self, x, until_test, prefix):
37         if until_test(x):
38             self.should_print = False
39         if self.should_print:
40             return x[0:len(prefix)] != prefix
41         return False
42
43 #
44 # Functions
45 #
46 __comment_prefix = 'STG:'
47 __patch_prefix = 'STG_PATCH:'
48
49 def __clean_comments(f):
50     """Removes lines marked for status in a commit file
51     """
52     f.seek(0)
53
54     # remove status-prefixed lines
55     lines = f.readlines()
56
57     patch_filter = FilterUntil()
58     until_test = lambda t: t == (__patch_prefix + '\n')
59     lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
60
61     # remove empty lines at the end
62     while len(lines) != 0 and lines[-1] == '\n':
63         del lines[-1]
64
65     f.seek(0); f.truncate()
66     f.writelines(lines)
67
68 def edit_file(series, line, comment, show_patch = True):
69     fname = '.stgitmsg.txt'
70     tmpl = templates.get_template('patchdescr.tmpl')
71
72     f = file(fname, 'w+')
73     if line:
74         print >> f, line
75     elif tmpl:
76         print >> f, tmpl,
77     else:
78         print >> f
79     print >> f, __comment_prefix, comment
80     print >> f, __comment_prefix, \
81           'Lines prefixed with "%s" will be automatically removed.' \
82           % __comment_prefix
83     print >> f, __comment_prefix, \
84           'Trailing empty lines will be automatically removed.'
85
86     if show_patch:
87        print >> f, __patch_prefix
88        # series.get_patch(series.get_current()).get_top()
89        git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
90
91     #Vim modeline must be near the end.
92     print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
93     f.close()
94
95     call_editor(fname)
96
97     f = file(fname, 'r+')
98
99     __clean_comments(f)
100     f.seek(0)
101     result = f.read()
102
103     f.close()
104     os.remove(fname)
105
106     return result
107
108 #
109 # Classes
110 #
111
112 class StgitObject:
113     """An object with stgit-like properties stored as files in a directory
114     """
115     def _set_dir(self, dir):
116         self.__dir = dir
117     def _dir(self):
118         return self.__dir
119
120     def create_empty_field(self, name):
121         create_empty_file(os.path.join(self.__dir, name))
122
123     def _get_field(self, name, multiline = False):
124         id_file = os.path.join(self.__dir, name)
125         if os.path.isfile(id_file):
126             line = read_string(id_file, multiline)
127             if line == '':
128                 return None
129             else:
130                 return line
131         else:
132             return None
133
134     def _set_field(self, name, value, multiline = False):
135         fname = os.path.join(self.__dir, name)
136         if value and value != '':
137             write_string(fname, value, multiline)
138         elif os.path.isfile(fname):
139             os.remove(fname)
140
141     
142 class Patch(StgitObject):
143     """Basic patch implementation
144     """
145     def __init__(self, name, series_dir, refs_dir):
146         self.__series_dir = series_dir
147         self.__name = name
148         self._set_dir(os.path.join(self.__series_dir, self.__name))
149         self.__refs_dir = refs_dir
150         self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
151         self.__log_ref_file = os.path.join(self.__refs_dir,
152                                            self.__name + '.log')
153
154     def create(self):
155         os.mkdir(self._dir())
156         self.create_empty_field('bottom')
157         self.create_empty_field('top')
158
159     def delete(self):
160         for f in os.listdir(self._dir()):
161             os.remove(os.path.join(self._dir(), f))
162         os.rmdir(self._dir())
163         os.remove(self.__top_ref_file)
164         if os.path.exists(self.__log_ref_file):
165             os.remove(self.__log_ref_file)
166
167     def get_name(self):
168         return self.__name
169
170     def rename(self, newname):
171         olddir = self._dir()
172         old_top_ref_file = self.__top_ref_file
173         old_log_ref_file = self.__log_ref_file
174         self.__name = newname
175         self._set_dir(os.path.join(self.__series_dir, self.__name))
176         self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
177         self.__log_ref_file = os.path.join(self.__refs_dir,
178                                            self.__name + '.log')
179
180         os.rename(olddir, self._dir())
181         os.rename(old_top_ref_file, self.__top_ref_file)
182         if os.path.exists(old_log_ref_file):
183             os.rename(old_log_ref_file, self.__log_ref_file)
184
185     def __update_top_ref(self, ref):
186         write_string(self.__top_ref_file, ref)
187
188     def __update_log_ref(self, ref):
189         write_string(self.__log_ref_file, ref)
190
191     def update_top_ref(self):
192         top = self.get_top()
193         if top:
194             self.__update_top_ref(top)
195
196     def get_old_bottom(self):
197         return self._get_field('bottom.old')
198
199     def get_bottom(self):
200         return self._get_field('bottom')
201
202     def set_bottom(self, value, backup = False):
203         if backup:
204             curr = self._get_field('bottom')
205             self._set_field('bottom.old', curr)
206         self._set_field('bottom', value)
207
208     def get_old_top(self):
209         return self._get_field('top.old')
210
211     def get_top(self):
212         return self._get_field('top')
213
214     def set_top(self, value, backup = False):
215         if backup:
216             curr = self._get_field('top')
217             self._set_field('top.old', curr)
218         self._set_field('top', value)
219         self.__update_top_ref(value)
220
221     def restore_old_boundaries(self):
222         bottom = self._get_field('bottom.old')
223         top = self._get_field('top.old')
224
225         if top and bottom:
226             self._set_field('bottom', bottom)
227             self._set_field('top', top)
228             self.__update_top_ref(top)
229             return True
230         else:
231             return False
232
233     def get_description(self):
234         return self._get_field('description', True)
235
236     def set_description(self, line):
237         self._set_field('description', line, True)
238
239     def get_authname(self):
240         return self._get_field('authname')
241
242     def set_authname(self, name):
243         self._set_field('authname', name or git.author().name)
244
245     def get_authemail(self):
246         return self._get_field('authemail')
247
248     def set_authemail(self, email):
249         self._set_field('authemail', email or git.author().email)
250
251     def get_authdate(self):
252         return self._get_field('authdate')
253
254     def set_authdate(self, date):
255         self._set_field('authdate', date or git.author().date)
256
257     def get_commname(self):
258         return self._get_field('commname')
259
260     def set_commname(self, name):
261         self._set_field('commname', name or git.committer().name)
262
263     def get_commemail(self):
264         return self._get_field('commemail')
265
266     def set_commemail(self, email):
267         self._set_field('commemail', email or git.committer().email)
268
269     def get_log(self):
270         return self._get_field('log')
271
272     def set_log(self, value, backup = False):
273         self._set_field('log', value)
274         self.__update_log_ref(value)
275
276
277 class Series(StgitObject):
278     """Class including the operations on series
279     """
280     def __init__(self, name = None):
281         """Takes a series name as the parameter.
282         """
283         try:
284             if name:
285                 self.__name = name
286             else:
287                 self.__name = git.get_head_file()
288             self.__base_dir = basedir.get()
289         except git.GitException, ex:
290             raise StackException, 'GIT tree not initialised: %s' % ex
291
292         self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
293         self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
294                                        self.__name)
295
296         self.__applied_file = os.path.join(self._dir(), 'applied')
297         self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
298         self.__hidden_file = os.path.join(self._dir(), 'hidden')
299
300         # where this series keeps its patches
301         self.__patch_dir = os.path.join(self._dir(), 'patches')
302         if not os.path.isdir(self.__patch_dir):
303             self.__patch_dir = self._dir()
304
305         # if no __refs_dir, create and populate it (upgrade old repositories)
306         if self.is_initialised() and not os.path.isdir(self.__refs_dir):
307             os.makedirs(self.__refs_dir)
308             for patch in self.get_applied() + self.get_unapplied():
309                 self.get_patch(patch).update_top_ref()
310
311         # trash directory
312         self.__trash_dir = os.path.join(self._dir(), 'trash')
313         if self.is_initialised() and not os.path.isdir(self.__trash_dir):
314             os.makedirs(self.__trash_dir)
315
316     def __patch_name_valid(self, name):
317         """Raise an exception if the patch name is not valid.
318         """
319         if not name or re.search('[^\w.-]', name):
320             raise StackException, 'Invalid patch name: "%s"' % name
321
322     def get_branch(self):
323         """Return the branch name for the Series object
324         """
325         return self.__name
326
327     def get_patch(self, name):
328         """Return a Patch object for the given name
329         """
330         return Patch(name, self.__patch_dir, self.__refs_dir)
331
332     def get_current_patch(self):
333         """Return a Patch object representing the topmost patch, or
334         None if there is no such patch."""
335         crt = self.get_current()
336         if not crt:
337             return None
338         return Patch(crt, self.__patch_dir, self.__refs_dir)
339
340     def get_current(self):
341         """Return the name of the topmost patch, or None if there is
342         no such patch."""
343         try:
344             applied = self.get_applied()
345         except StackException:
346             # No "applied" file: branch is not initialized.
347             return None
348         try:
349             return applied[-1]
350         except IndexError:
351             # No patches applied.
352             return None
353
354     def get_applied(self):
355         if not os.path.isfile(self.__applied_file):
356             raise StackException, 'Branch "%s" not initialised' % self.__name
357         f = file(self.__applied_file)
358         names = [line.strip() for line in f.readlines()]
359         f.close()
360         return names
361
362     def get_unapplied(self):
363         if not os.path.isfile(self.__unapplied_file):
364             raise StackException, 'Branch "%s" not initialised' % self.__name
365         f = file(self.__unapplied_file)
366         names = [line.strip() for line in f.readlines()]
367         f.close()
368         return names
369
370     def get_hidden(self):
371         if not os.path.isfile(self.__hidden_file):
372             return []
373         f = file(self.__hidden_file)
374         names = [line.strip() for line in f.readlines()]
375         f.close()
376         return names
377
378     def get_base(self):
379         # Return the parent of the bottommost patch, if there is one.
380         if os.path.isfile(self.__applied_file):
381             bottommost = file(self.__applied_file).readline().strip()
382             if bottommost:
383                 return self.get_patch(bottommost).get_bottom()
384         # No bottommost patch, so just return HEAD
385         return git.get_head()
386
387     def get_head(self):
388         """Return the head of the branch
389         """
390         crt = self.get_current_patch()
391         if crt:
392             return crt.get_top()
393         else:
394             return self.get_base()
395
396     def get_protected(self):
397         return os.path.isfile(os.path.join(self._dir(), 'protected'))
398
399     def protect(self):
400         protect_file = os.path.join(self._dir(), 'protected')
401         if not os.path.isfile(protect_file):
402             create_empty_file(protect_file)
403
404     def unprotect(self):
405         protect_file = os.path.join(self._dir(), 'protected')
406         if os.path.isfile(protect_file):
407             os.remove(protect_file)
408
409     def __branch_descr(self):
410         return 'branch.%s.description' % self.get_branch()
411
412     def get_description(self):
413         # Fall back to the .git/patches/<branch>/description file if
414         # the config variable is unset.
415         return (config.get(self.__branch_descr())
416                 or self._get_field('description') or '')
417
418     def set_description(self, line):
419         if line:
420             config.set(self.__branch_descr(), line)
421         else:
422             config.unset(self.__branch_descr())
423         # Delete the old .git/patches/<branch>/description file if it
424         # exists.
425         self._set_field('description', None)
426
427     def get_parent_remote(self):
428         value = config.get('branch.%s.remote' % self.__name)
429         if value:
430             return value
431         elif 'origin' in git.remotes_list():
432             print 'Notice: no parent remote declared for stack "%s", ' \
433                   'defaulting to "origin". Consider setting "branch.%s.remote" ' \
434                   'and "branch.%s.merge" with "git repo-config".' \
435                   % (self.__name, self.__name, self.__name)
436             return 'origin'
437         else:
438             raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
439
440     def __set_parent_remote(self, remote):
441         value = config.set('branch.%s.remote' % self.__name, remote)
442
443     def get_parent_branch(self):
444         value = config.get('branch.%s.stgit.parentbranch' % self.__name)
445         if value:
446             return value
447         elif git.rev_parse('heads/origin'):
448             print 'Notice: no parent branch declared for stack "%s", ' \
449                   'defaulting to "heads/origin". Consider setting ' \
450                   '"branch.%s.stgit.parentbranch" with "git repo-config".' \
451                   % (self.__name, self.__name)
452             return 'heads/origin'
453         else:
454             raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
455
456     def __set_parent_branch(self, name):
457         if config.get('branch.%s.remote' % self.__name):
458             # Never set merge if remote is not set to avoid
459             # possibly-erroneous lookups into 'origin'
460             config.set('branch.%s.merge' % self.__name, name)
461         config.set('branch.%s.stgit.parentbranch' % self.__name, name)
462
463     def set_parent(self, remote, localbranch):
464         if localbranch:
465             self.__set_parent_remote(remote)
466             self.__set_parent_branch(localbranch)
467         # We'll enforce this later
468 #         else:
469 #             raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
470
471     def __patch_is_current(self, patch):
472         return patch.get_name() == self.get_current()
473
474     def patch_applied(self, name):
475         """Return true if the patch exists in the applied list
476         """
477         return name in self.get_applied()
478
479     def patch_unapplied(self, name):
480         """Return true if the patch exists in the unapplied list
481         """
482         return name in self.get_unapplied()
483
484     def patch_hidden(self, name):
485         """Return true if the patch is hidden.
486         """
487         return name in self.get_hidden()
488
489     def patch_exists(self, name):
490         """Return true if there is a patch with the given name, false
491         otherwise."""
492         return self.patch_applied(name) or self.patch_unapplied(name)
493
494     def head_top_equal(self):
495         """Return true if the head and the top are the same
496         """
497         crt = self.get_current_patch()
498         if not crt:
499             # we don't care, no patches applied
500             return True
501         return git.get_head() == crt.get_top()
502
503     def is_initialised(self):
504         """Checks if series is already initialised
505         """
506         return os.path.isdir(self.__patch_dir)
507
508     def init(self, create_at=False, parent_remote=None, parent_branch=None):
509         """Initialises the stgit series
510         """
511         if os.path.exists(self.__patch_dir):
512             raise StackException, self.__patch_dir + ' already exists'
513         if os.path.exists(self.__refs_dir):
514             raise StackException, self.__refs_dir + ' already exists'
515
516         if (create_at!=False):
517             git.create_branch(self.__name, create_at)
518
519         os.makedirs(self.__patch_dir)
520
521         self.set_parent(parent_remote, parent_branch)
522
523         self.create_empty_field('applied')
524         self.create_empty_field('unapplied')
525         os.makedirs(os.path.join(self._dir(), 'patches'))
526         os.makedirs(self.__refs_dir)
527         self._set_field('orig-base', git.get_head())
528
529     def convert(self):
530         """Either convert to use a separate patch directory, or
531         unconvert to place the patches in the same directory with
532         series control files
533         """
534         if self.__patch_dir == self._dir():
535             print 'Converting old-style to new-style...',
536             sys.stdout.flush()
537
538             self.__patch_dir = os.path.join(self._dir(), 'patches')
539             os.makedirs(self.__patch_dir)
540
541             for p in self.get_applied() + self.get_unapplied():
542                 src = os.path.join(self._dir(), p)
543                 dest = os.path.join(self.__patch_dir, p)
544                 os.rename(src, dest)
545
546             print 'done'
547
548         else:
549             print 'Converting new-style to old-style...',
550             sys.stdout.flush()
551
552             for p in self.get_applied() + self.get_unapplied():
553                 src = os.path.join(self.__patch_dir, p)
554                 dest = os.path.join(self._dir(), p)
555                 os.rename(src, dest)
556
557             if not os.listdir(self.__patch_dir):
558                 os.rmdir(self.__patch_dir)
559                 print 'done'
560             else:
561                 print 'Patch directory %s is not empty.' % self.__patch_dir
562
563             self.__patch_dir = self._dir()
564
565     def rename(self, to_name):
566         """Renames a series
567         """
568         to_stack = Series(to_name)
569
570         if to_stack.is_initialised():
571             raise StackException, '"%s" already exists' % to_stack.get_branch()
572
573         git.rename_branch(self.__name, to_name)
574
575         if os.path.isdir(self._dir()):
576             rename(os.path.join(self.__base_dir, 'patches'),
577                    self.__name, to_stack.__name)
578         if os.path.exists(self.__refs_dir):
579             rename(os.path.join(self.__base_dir, 'refs', 'patches'),
580                    self.__name, to_stack.__name)
581
582         # Rename the config section
583         config.rename_section("branch.%s" % self.__name,
584                               "branch.%s" % to_name)
585
586         self.__init__(to_name)
587
588     def clone(self, target_series):
589         """Clones a series
590         """
591         try:
592             # allow cloning of branches not under StGIT control
593             base = self.get_base()
594         except:
595             base = git.get_head()
596         Series(target_series).init(create_at = base)
597         new_series = Series(target_series)
598
599         # generate an artificial description file
600         new_series.set_description('clone of "%s"' % self.__name)
601
602         # clone self's entire series as unapplied patches
603         try:
604             # allow cloning of branches not under StGIT control
605             applied = self.get_applied()
606             unapplied = self.get_unapplied()
607             patches = applied + unapplied
608             patches.reverse()
609         except:
610             patches = applied = unapplied = []
611         for p in patches:
612             patch = self.get_patch(p)
613             newpatch = new_series.new_patch(p, message = patch.get_description(),
614                                             can_edit = False, unapplied = True,
615                                             bottom = patch.get_bottom(),
616                                             top = patch.get_top(),
617                                             author_name = patch.get_authname(),
618                                             author_email = patch.get_authemail(),
619                                             author_date = patch.get_authdate())
620             if patch.get_log():
621                 print "setting log to %s" %  patch.get_log()
622                 newpatch.set_log(patch.get_log())
623             else:
624                 print "no log for %s" % p
625
626         # fast forward the cloned series to self's top
627         new_series.forward_patches(applied)
628
629         # Clone parent informations
630         value = config.get('branch.%s.remote' % self.__name)
631         if value:
632             config.set('branch.%s.remote' % target_series, value)
633
634         value = config.get('branch.%s.merge' % self.__name)
635         if value:
636             config.set('branch.%s.merge' % target_series, value)
637
638         value = config.get('branch.%s.stgit.parentbranch' % self.__name)
639         if value:
640             config.set('branch.%s.stgit.parentbranch' % target_series, value)
641
642     def delete(self, force = False):
643         """Deletes an stgit series
644         """
645         if self.is_initialised():
646             patches = self.get_unapplied() + self.get_applied()
647             if not force and patches:
648                 raise StackException, \
649                       'Cannot delete: the series still contains patches'
650             for p in patches:
651                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
652
653             # remove the trash directory
654             for fname in os.listdir(self.__trash_dir):
655                 os.remove(os.path.join(self.__trash_dir, fname))
656             os.rmdir(self.__trash_dir)
657
658             # FIXME: find a way to get rid of those manual removals
659             # (move functionality to StgitObject ?)
660             if os.path.exists(self.__applied_file):
661                 os.remove(self.__applied_file)
662             if os.path.exists(self.__unapplied_file):
663                 os.remove(self.__unapplied_file)
664             if os.path.exists(self.__hidden_file):
665                 os.remove(self.__hidden_file)
666             if os.path.exists(self._dir()+'/orig-base'):
667                 os.remove(self._dir()+'/orig-base')
668
669             # Remove obsolete files that StGIT no longer uses, but
670             # that might still be around if this is an old repository.
671             for obsolete in ([os.path.join(self._dir(), fn)
672                               for fn in ['current', 'description']]
673                              + [os.path.join(self.__base_dir,
674                                              'refs', 'bases', self.__name)]):
675                 if os.path.exists(obsolete):
676                     os.remove(obsolete)
677
678             if not os.listdir(self.__patch_dir):
679                 os.rmdir(self.__patch_dir)
680             else:
681                 print 'Patch directory %s is not empty.' % self.__patch_dir
682
683             try:
684                 os.removedirs(self._dir())
685             except OSError:
686                 raise StackException, 'Series directory %s is not empty.' % self._dir()
687
688             try:
689                 os.removedirs(self.__refs_dir)
690             except OSError:
691                 print 'Refs directory %s is not empty.' % self.__refs_dir
692
693         # Cleanup parent informations
694         # FIXME: should one day make use of git-config --section-remove,
695         # scheduled for 1.5.1
696         config.unset('branch.%s.remote' % self.__name)
697         config.unset('branch.%s.merge' % self.__name)
698         config.unset('branch.%s.stgit.parentbranch' % 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 = descr.rstrip()
740             if descr.find("\nSigned-off-by:") < 0 \
741                and descr.find("\nAcked-by:") < 0:
742                 descr = descr + "\n"
743
744             descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
745                                            committer_name, committer_email)
746
747         bottom = patch.get_bottom()
748
749         commit_id = git.commit(files = files,
750                                message = descr, parents = [bottom],
751                                cache_update = cache_update,
752                                allowempty = True,
753                                author_name = author_name,
754                                author_email = author_email,
755                                author_date = author_date,
756                                committer_name = committer_name,
757                                committer_email = committer_email)
758
759         patch.set_bottom(bottom, backup = backup)
760         patch.set_top(commit_id, backup = backup)
761         patch.set_description(descr)
762         patch.set_authname(author_name)
763         patch.set_authemail(author_email)
764         patch.set_authdate(author_date)
765         patch.set_commname(committer_name)
766         patch.set_commemail(committer_email)
767
768         if log:
769             self.log_patch(patch, log)
770
771         return commit_id
772
773     def undo_refresh(self):
774         """Undo the patch boundaries changes caused by 'refresh'
775         """
776         name = self.get_current()
777         assert(name)
778
779         patch = Patch(name, self.__patch_dir, self.__refs_dir)
780         old_bottom = patch.get_old_bottom()
781         old_top = patch.get_old_top()
782
783         # the bottom of the patch is not changed by refresh. If the
784         # old_bottom is different, there wasn't any previous 'refresh'
785         # command (probably only a 'push')
786         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
787             raise StackException, 'No undo information available'
788
789         git.reset(tree_id = old_top, check_out = False)
790         if patch.restore_old_boundaries():
791             self.log_patch(patch, 'undo')
792
793     def new_patch(self, name, message = None, can_edit = True,
794                   unapplied = False, show_patch = False,
795                   top = None, bottom = None,
796                   author_name = None, author_email = None, author_date = None,
797                   committer_name = None, committer_email = None,
798                   before_existing = False, refresh = True):
799         """Creates a new patch
800         """
801
802         if name != None:
803             self.__patch_name_valid(name)
804             if self.patch_applied(name) or self.patch_unapplied(name):
805                 raise StackException, 'Patch "%s" already exists' % name
806
807         if not message and can_edit:
808             descr = edit_file(
809                 self, None,
810                 'Please enter the description for the patch above.',
811                 show_patch)
812         else:
813             descr = message
814
815         head = git.get_head()
816
817         if name == None:
818             name = make_patch_name(descr, self.patch_exists)
819
820         patch = Patch(name, self.__patch_dir, self.__refs_dir)
821         patch.create()
822
823         if bottom:
824             patch.set_bottom(bottom)
825         else:
826             patch.set_bottom(head)
827         if top:
828             patch.set_top(top)
829         else:
830             patch.set_top(head)
831
832         patch.set_description(descr)
833         patch.set_authname(author_name)
834         patch.set_authemail(author_email)
835         patch.set_authdate(author_date)
836         patch.set_commname(committer_name)
837         patch.set_commemail(committer_email)
838
839         if unapplied:
840             self.log_patch(patch, 'new')
841
842             patches = [patch.get_name()] + self.get_unapplied()
843
844             f = file(self.__unapplied_file, 'w+')
845             f.writelines([line + '\n' for line in patches])
846             f.close()
847         elif before_existing:
848             self.log_patch(patch, 'new')
849
850             insert_string(self.__applied_file, patch.get_name())
851         else:
852             append_string(self.__applied_file, patch.get_name())
853             if refresh:
854                 self.refresh_patch(cache_update = False, log = 'new')
855
856         return patch
857
858     def delete_patch(self, name):
859         """Deletes a patch
860         """
861         self.__patch_name_valid(name)
862         patch = Patch(name, self.__patch_dir, self.__refs_dir)
863
864         if self.__patch_is_current(patch):
865             self.pop_patch(name)
866         elif self.patch_applied(name):
867             raise StackException, 'Cannot remove an applied patch, "%s", ' \
868                   'which is not current' % name
869         elif not name in self.get_unapplied():
870             raise StackException, 'Unknown patch "%s"' % name
871
872         # save the commit id to a trash file
873         write_string(os.path.join(self.__trash_dir, name), patch.get_top())
874
875         patch.delete()
876
877         unapplied = self.get_unapplied()
878         unapplied.remove(name)
879         f = file(self.__unapplied_file, 'w+')
880         f.writelines([line + '\n' for line in unapplied])
881         f.close()
882
883         if self.patch_hidden(name):
884             self.unhide_patch(name)
885
886     def forward_patches(self, names):
887         """Try to fast-forward an array of patches.
888
889         On return, patches in names[0:returned_value] have been pushed on the
890         stack. Apply the rest with push_patch
891         """
892         unapplied = self.get_unapplied()
893
894         forwarded = 0
895         top = git.get_head()
896
897         for name in names:
898             assert(name in unapplied)
899
900             patch = Patch(name, self.__patch_dir, self.__refs_dir)
901
902             head = top
903             bottom = patch.get_bottom()
904             top = patch.get_top()
905
906             # top != bottom always since we have a commit for each patch
907             if head == bottom:
908                 # reset the backup information. No logging since the
909                 # patch hasn't changed
910                 patch.set_bottom(head, backup = True)
911                 patch.set_top(top, backup = True)
912
913             else:
914                 head_tree = git.get_commit(head).get_tree()
915                 bottom_tree = git.get_commit(bottom).get_tree()
916                 if head_tree == bottom_tree:
917                     # We must just reparent this patch and create a new commit
918                     # for it
919                     descr = patch.get_description()
920                     author_name = patch.get_authname()
921                     author_email = patch.get_authemail()
922                     author_date = patch.get_authdate()
923                     committer_name = patch.get_commname()
924                     committer_email = patch.get_commemail()
925
926                     top_tree = git.get_commit(top).get_tree()
927
928                     top = git.commit(message = descr, parents = [head],
929                                      cache_update = False,
930                                      tree_id = top_tree,
931                                      allowempty = True,
932                                      author_name = author_name,
933                                      author_email = author_email,
934                                      author_date = author_date,
935                                      committer_name = committer_name,
936                                      committer_email = committer_email)
937
938                     patch.set_bottom(head, backup = True)
939                     patch.set_top(top, backup = True)
940
941                     self.log_patch(patch, 'push(f)')
942                 else:
943                     top = head
944                     # stop the fast-forwarding, must do a real merge
945                     break
946
947             forwarded+=1
948             unapplied.remove(name)
949
950         if forwarded == 0:
951             return 0
952
953         git.switch(top)
954
955         append_strings(self.__applied_file, names[0:forwarded])
956
957         f = file(self.__unapplied_file, 'w+')
958         f.writelines([line + '\n' for line in unapplied])
959         f.close()
960
961         return forwarded
962
963     def merged_patches(self, names):
964         """Test which patches were merged upstream by reverse-applying
965         them in reverse order. The function returns the list of
966         patches detected to have been applied. The state of the tree
967         is restored to the original one
968         """
969         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
970                    for name in names]
971         patches.reverse()
972
973         merged = []
974         for p in patches:
975             if git.apply_diff(p.get_top(), p.get_bottom()):
976                 merged.append(p.get_name())
977         merged.reverse()
978
979         git.reset()
980
981         return merged
982
983     def push_patch(self, name, empty = False):
984         """Pushes a patch on the stack
985         """
986         unapplied = self.get_unapplied()
987         assert(name in unapplied)
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 or ' \
1033                           'revert the operation with "push --undo".'
1034
1035         append_string(self.__applied_file, name)
1036
1037         unapplied.remove(name)
1038         f = file(self.__unapplied_file, 'w+')
1039         f.writelines([line + '\n' for line in unapplied])
1040         f.close()
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         if git.get_head_file() == self.get_branch():
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             git.switch(patch.get_bottom(), keep)
1099         else:
1100             git.set_branch(self.get_branch(), patch.get_bottom())
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     def empty_patch(self, name):
1121         """Returns True if the patch is empty
1122         """
1123         self.__patch_name_valid(name)
1124         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1125         bottom = patch.get_bottom()
1126         top = patch.get_top()
1127
1128         if bottom == top:
1129             return True
1130         elif git.get_commit(top).get_tree() \
1131                  == git.get_commit(bottom).get_tree():
1132             return True
1133
1134         return False
1135
1136     def rename_patch(self, oldname, newname):
1137         self.__patch_name_valid(newname)
1138
1139         applied = self.get_applied()
1140         unapplied = self.get_unapplied()
1141
1142         if oldname == newname:
1143             raise StackException, '"To" name and "from" name are the same'
1144
1145         if newname in applied or newname in unapplied:
1146             raise StackException, 'Patch "%s" already exists' % newname
1147
1148         if self.patch_hidden(oldname):
1149             self.unhide_patch(oldname)
1150             self.hide_patch(newname)
1151
1152         if oldname in unapplied:
1153             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1154             unapplied[unapplied.index(oldname)] = newname
1155
1156             f = file(self.__unapplied_file, 'w+')
1157             f.writelines([line + '\n' for line in unapplied])
1158             f.close()
1159         elif oldname in applied:
1160             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1161
1162             applied[applied.index(oldname)] = newname
1163
1164             f = file(self.__applied_file, 'w+')
1165             f.writelines([line + '\n' for line in applied])
1166             f.close()
1167         else:
1168             raise StackException, 'Unknown patch "%s"' % oldname
1169
1170     def log_patch(self, patch, message):
1171         """Generate a log commit for a patch
1172         """
1173         top = git.get_commit(patch.get_top())
1174         msg = '%s\t%s' % (message, top.get_id_hash())
1175
1176         old_log = patch.get_log()
1177         if old_log:
1178             parents = [old_log]
1179         else:
1180             parents = []
1181
1182         log = git.commit(message = msg, parents = parents,
1183                          cache_update = False, tree_id = top.get_tree(),
1184                          allowempty = True)
1185         patch.set_log(log)
1186
1187     def hide_patch(self, name):
1188         """Add the patch to the hidden list.
1189         """
1190         if not self.patch_exists(name):
1191             raise StackException, 'Unknown patch "%s"' % name
1192         elif self.patch_hidden(name):
1193             raise StackException, 'Patch "%s" already hidden' % name
1194
1195         append_string(self.__hidden_file, name)
1196
1197     def unhide_patch(self, name):
1198         """Add the patch to the hidden list.
1199         """
1200         if not self.patch_exists(name):
1201             raise StackException, 'Unknown patch "%s"' % name
1202         hidden = self.get_hidden()
1203         if not name in hidden:
1204             raise StackException, 'Patch "%s" not hidden' % name
1205
1206         hidden.remove(name)
1207
1208         f = file(self.__hidden_file, 'w+')
1209         f.writelines([line + '\n' for line in hidden])
1210         f.close()