chiark / gitweb /
5e8d2d718228e46eab70202c4603e23cf0f587b7
[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             if not os.listdir(self.__patch_dir):
670                 os.rmdir(self.__patch_dir)
671             else:
672                 print 'Patch directory %s is not empty.' % self.__patch_dir
673
674             try:
675                 os.removedirs(self._dir())
676             except OSError:
677                 raise StackException, 'Series directory %s is not empty.' % self._dir()
678
679             try:
680                 os.removedirs(self.__refs_dir)
681             except OSError:
682                 print 'Refs directory %s is not empty.' % self.__refs_dir
683
684         # Cleanup parent informations
685         # FIXME: should one day make use of git-config --section-remove,
686         # scheduled for 1.5.1
687         config.unset('branch.%s.remote' % self.__name)
688         config.unset('branch.%s.merge' % self.__name)
689         config.unset('branch.%s.stgit.parentbranch' % self.__name)
690
691     def refresh_patch(self, files = None, message = None, edit = False,
692                       show_patch = False,
693                       cache_update = True,
694                       author_name = None, author_email = None,
695                       author_date = None,
696                       committer_name = None, committer_email = None,
697                       backup = False, sign_str = None, log = 'refresh'):
698         """Generates a new commit for the given patch
699         """
700         name = self.get_current()
701         if not name:
702             raise StackException, 'No patches applied'
703
704         patch = Patch(name, self.__patch_dir, self.__refs_dir)
705
706         descr = patch.get_description()
707         if not (message or descr):
708             edit = True
709             descr = ''
710         elif message:
711             descr = message
712
713         if not message and edit:
714             descr = edit_file(self, descr.rstrip(), \
715                               'Please edit the description for patch "%s" ' \
716                               'above.' % name, show_patch)
717
718         if not author_name:
719             author_name = patch.get_authname()
720         if not author_email:
721             author_email = patch.get_authemail()
722         if not author_date:
723             author_date = patch.get_authdate()
724         if not committer_name:
725             committer_name = patch.get_commname()
726         if not committer_email:
727             committer_email = patch.get_commemail()
728
729         if sign_str:
730             descr = descr.rstrip()
731             if descr.find("\nSigned-off-by:") < 0 \
732                and descr.find("\nAcked-by:") < 0:
733                 descr = descr + "\n"
734
735             descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
736                                            committer_name, committer_email)
737
738         bottom = patch.get_bottom()
739
740         commit_id = git.commit(files = files,
741                                message = descr, parents = [bottom],
742                                cache_update = cache_update,
743                                allowempty = True,
744                                author_name = author_name,
745                                author_email = author_email,
746                                author_date = author_date,
747                                committer_name = committer_name,
748                                committer_email = committer_email)
749
750         patch.set_bottom(bottom, backup = backup)
751         patch.set_top(commit_id, backup = backup)
752         patch.set_description(descr)
753         patch.set_authname(author_name)
754         patch.set_authemail(author_email)
755         patch.set_authdate(author_date)
756         patch.set_commname(committer_name)
757         patch.set_commemail(committer_email)
758
759         if log:
760             self.log_patch(patch, log)
761
762         return commit_id
763
764     def undo_refresh(self):
765         """Undo the patch boundaries changes caused by 'refresh'
766         """
767         name = self.get_current()
768         assert(name)
769
770         patch = Patch(name, self.__patch_dir, self.__refs_dir)
771         old_bottom = patch.get_old_bottom()
772         old_top = patch.get_old_top()
773
774         # the bottom of the patch is not changed by refresh. If the
775         # old_bottom is different, there wasn't any previous 'refresh'
776         # command (probably only a 'push')
777         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
778             raise StackException, 'No undo information available'
779
780         git.reset(tree_id = old_top, check_out = False)
781         if patch.restore_old_boundaries():
782             self.log_patch(patch, 'undo')
783
784     def new_patch(self, name, message = None, can_edit = True,
785                   unapplied = False, show_patch = False,
786                   top = None, bottom = None,
787                   author_name = None, author_email = None, author_date = None,
788                   committer_name = None, committer_email = None,
789                   before_existing = False, refresh = True):
790         """Creates a new patch
791         """
792
793         if name != None:
794             self.__patch_name_valid(name)
795             if self.patch_applied(name) or self.patch_unapplied(name):
796                 raise StackException, 'Patch "%s" already exists' % name
797
798         if not message and can_edit:
799             descr = edit_file(
800                 self, None,
801                 'Please enter the description for the patch above.',
802                 show_patch)
803         else:
804             descr = message
805
806         head = git.get_head()
807
808         if name == None:
809             name = make_patch_name(descr, self.patch_exists)
810
811         patch = Patch(name, self.__patch_dir, self.__refs_dir)
812         patch.create()
813
814         if bottom:
815             patch.set_bottom(bottom)
816         else:
817             patch.set_bottom(head)
818         if top:
819             patch.set_top(top)
820         else:
821             patch.set_top(head)
822
823         patch.set_description(descr)
824         patch.set_authname(author_name)
825         patch.set_authemail(author_email)
826         patch.set_authdate(author_date)
827         patch.set_commname(committer_name)
828         patch.set_commemail(committer_email)
829
830         if unapplied:
831             self.log_patch(patch, 'new')
832
833             patches = [patch.get_name()] + self.get_unapplied()
834
835             f = file(self.__unapplied_file, 'w+')
836             f.writelines([line + '\n' for line in patches])
837             f.close()
838         elif before_existing:
839             self.log_patch(patch, 'new')
840
841             insert_string(self.__applied_file, patch.get_name())
842         else:
843             append_string(self.__applied_file, patch.get_name())
844             if refresh:
845                 self.refresh_patch(cache_update = False, log = 'new')
846
847         return patch
848
849     def delete_patch(self, name):
850         """Deletes a patch
851         """
852         self.__patch_name_valid(name)
853         patch = Patch(name, self.__patch_dir, self.__refs_dir)
854
855         if self.__patch_is_current(patch):
856             self.pop_patch(name)
857         elif self.patch_applied(name):
858             raise StackException, 'Cannot remove an applied patch, "%s", ' \
859                   'which is not current' % name
860         elif not name in self.get_unapplied():
861             raise StackException, 'Unknown patch "%s"' % name
862
863         # save the commit id to a trash file
864         write_string(os.path.join(self.__trash_dir, name), patch.get_top())
865
866         patch.delete()
867
868         unapplied = self.get_unapplied()
869         unapplied.remove(name)
870         f = file(self.__unapplied_file, 'w+')
871         f.writelines([line + '\n' for line in unapplied])
872         f.close()
873
874         if self.patch_hidden(name):
875             self.unhide_patch(name)
876
877     def forward_patches(self, names):
878         """Try to fast-forward an array of patches.
879
880         On return, patches in names[0:returned_value] have been pushed on the
881         stack. Apply the rest with push_patch
882         """
883         unapplied = self.get_unapplied()
884
885         forwarded = 0
886         top = git.get_head()
887
888         for name in names:
889             assert(name in unapplied)
890
891             patch = Patch(name, self.__patch_dir, self.__refs_dir)
892
893             head = top
894             bottom = patch.get_bottom()
895             top = patch.get_top()
896
897             # top != bottom always since we have a commit for each patch
898             if head == bottom:
899                 # reset the backup information. No logging since the
900                 # patch hasn't changed
901                 patch.set_bottom(head, backup = True)
902                 patch.set_top(top, backup = True)
903
904             else:
905                 head_tree = git.get_commit(head).get_tree()
906                 bottom_tree = git.get_commit(bottom).get_tree()
907                 if head_tree == bottom_tree:
908                     # We must just reparent this patch and create a new commit
909                     # for it
910                     descr = patch.get_description()
911                     author_name = patch.get_authname()
912                     author_email = patch.get_authemail()
913                     author_date = patch.get_authdate()
914                     committer_name = patch.get_commname()
915                     committer_email = patch.get_commemail()
916
917                     top_tree = git.get_commit(top).get_tree()
918
919                     top = git.commit(message = descr, parents = [head],
920                                      cache_update = False,
921                                      tree_id = top_tree,
922                                      allowempty = True,
923                                      author_name = author_name,
924                                      author_email = author_email,
925                                      author_date = author_date,
926                                      committer_name = committer_name,
927                                      committer_email = committer_email)
928
929                     patch.set_bottom(head, backup = True)
930                     patch.set_top(top, backup = True)
931
932                     self.log_patch(patch, 'push(f)')
933                 else:
934                     top = head
935                     # stop the fast-forwarding, must do a real merge
936                     break
937
938             forwarded+=1
939             unapplied.remove(name)
940
941         if forwarded == 0:
942             return 0
943
944         git.switch(top)
945
946         append_strings(self.__applied_file, names[0:forwarded])
947
948         f = file(self.__unapplied_file, 'w+')
949         f.writelines([line + '\n' for line in unapplied])
950         f.close()
951
952         return forwarded
953
954     def merged_patches(self, names):
955         """Test which patches were merged upstream by reverse-applying
956         them in reverse order. The function returns the list of
957         patches detected to have been applied. The state of the tree
958         is restored to the original one
959         """
960         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
961                    for name in names]
962         patches.reverse()
963
964         merged = []
965         for p in patches:
966             if git.apply_diff(p.get_top(), p.get_bottom()):
967                 merged.append(p.get_name())
968         merged.reverse()
969
970         git.reset()
971
972         return merged
973
974     def push_patch(self, name, empty = False):
975         """Pushes a patch on the stack
976         """
977         unapplied = self.get_unapplied()
978         assert(name in unapplied)
979
980         patch = Patch(name, self.__patch_dir, self.__refs_dir)
981
982         head = git.get_head()
983         bottom = patch.get_bottom()
984         top = patch.get_top()
985
986         ex = None
987         modified = False
988
989         # top != bottom always since we have a commit for each patch
990         if empty:
991             # just make an empty patch (top = bottom = HEAD). This
992             # option is useful to allow undoing already merged
993             # patches. The top is updated by refresh_patch since we
994             # need an empty commit
995             patch.set_bottom(head, backup = True)
996             patch.set_top(head, backup = True)
997             modified = True
998         elif head == bottom:
999             # reset the backup information. No need for logging
1000             patch.set_bottom(bottom, backup = True)
1001             patch.set_top(top, backup = True)
1002
1003             git.switch(top)
1004         else:
1005             # new patch needs to be refreshed.
1006             # The current patch is empty after merge.
1007             patch.set_bottom(head, backup = True)
1008             patch.set_top(head, backup = True)
1009
1010             # Try the fast applying first. If this fails, fall back to the
1011             # three-way merge
1012             if not git.apply_diff(bottom, top):
1013                 # if git.apply_diff() fails, the patch requires a diff3
1014                 # merge and can be reported as modified
1015                 modified = True
1016
1017                 # merge can fail but the patch needs to be pushed
1018                 try:
1019                     git.merge(bottom, head, top, recursive = True)
1020                 except git.GitException, ex:
1021                     print >> sys.stderr, \
1022                           'The merge failed during "push". ' \
1023                           'Use "refresh" after fixing the conflicts or ' \
1024                           'revert the operation with "push --undo".'
1025
1026         append_string(self.__applied_file, name)
1027
1028         unapplied.remove(name)
1029         f = file(self.__unapplied_file, 'w+')
1030         f.writelines([line + '\n' for line in unapplied])
1031         f.close()
1032
1033         # head == bottom case doesn't need to refresh the patch
1034         if empty or head != bottom:
1035             if not ex:
1036                 # if the merge was OK and no conflicts, just refresh the patch
1037                 # The GIT cache was already updated by the merge operation
1038                 if modified:
1039                     log = 'push(m)'
1040                 else:
1041                     log = 'push'
1042                 self.refresh_patch(cache_update = False, log = log)
1043             else:
1044                 # we store the correctly merged files only for
1045                 # tracking the conflict history. Note that the
1046                 # git.merge() operations should always leave the index
1047                 # in a valid state (i.e. only stage 0 files)
1048                 self.refresh_patch(cache_update = False, log = 'push(c)')
1049                 raise StackException, str(ex)
1050
1051         return modified
1052
1053     def undo_push(self):
1054         name = self.get_current()
1055         assert(name)
1056
1057         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1058         old_bottom = patch.get_old_bottom()
1059         old_top = patch.get_old_top()
1060
1061         # the top of the patch is changed by a push operation only
1062         # together with the bottom (otherwise the top was probably
1063         # modified by 'refresh'). If they are both unchanged, there
1064         # was a fast forward
1065         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1066             raise StackException, 'No undo information available'
1067
1068         git.reset()
1069         self.pop_patch(name)
1070         ret = patch.restore_old_boundaries()
1071         if ret:
1072             self.log_patch(patch, 'undo')
1073
1074         return ret
1075
1076     def pop_patch(self, name, keep = False):
1077         """Pops the top patch from the stack
1078         """
1079         applied = self.get_applied()
1080         applied.reverse()
1081         assert(name in applied)
1082
1083         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1084
1085         if git.get_head_file() == self.get_branch():
1086             if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1087                 raise StackException(
1088                     'Failed to pop patches while preserving the local changes')
1089             git.switch(patch.get_bottom(), keep)
1090         else:
1091             git.set_branch(self.get_branch(), patch.get_bottom())
1092
1093         # save the new applied list
1094         idx = applied.index(name) + 1
1095
1096         popped = applied[:idx]
1097         popped.reverse()
1098         unapplied = popped + self.get_unapplied()
1099
1100         f = file(self.__unapplied_file, 'w+')
1101         f.writelines([line + '\n' for line in unapplied])
1102         f.close()
1103
1104         del applied[:idx]
1105         applied.reverse()
1106
1107         f = file(self.__applied_file, 'w+')
1108         f.writelines([line + '\n' for line in applied])
1109         f.close()
1110
1111     def empty_patch(self, name):
1112         """Returns True if the patch is empty
1113         """
1114         self.__patch_name_valid(name)
1115         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1116         bottom = patch.get_bottom()
1117         top = patch.get_top()
1118
1119         if bottom == top:
1120             return True
1121         elif git.get_commit(top).get_tree() \
1122                  == git.get_commit(bottom).get_tree():
1123             return True
1124
1125         return False
1126
1127     def rename_patch(self, oldname, newname):
1128         self.__patch_name_valid(newname)
1129
1130         applied = self.get_applied()
1131         unapplied = self.get_unapplied()
1132
1133         if oldname == newname:
1134             raise StackException, '"To" name and "from" name are the same'
1135
1136         if newname in applied or newname in unapplied:
1137             raise StackException, 'Patch "%s" already exists' % newname
1138
1139         if self.patch_hidden(oldname):
1140             self.unhide_patch(oldname)
1141             self.hide_patch(newname)
1142
1143         if oldname in unapplied:
1144             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1145             unapplied[unapplied.index(oldname)] = newname
1146
1147             f = file(self.__unapplied_file, 'w+')
1148             f.writelines([line + '\n' for line in unapplied])
1149             f.close()
1150         elif oldname in applied:
1151             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1152
1153             applied[applied.index(oldname)] = newname
1154
1155             f = file(self.__applied_file, 'w+')
1156             f.writelines([line + '\n' for line in applied])
1157             f.close()
1158         else:
1159             raise StackException, 'Unknown patch "%s"' % oldname
1160
1161     def log_patch(self, patch, message):
1162         """Generate a log commit for a patch
1163         """
1164         top = git.get_commit(patch.get_top())
1165         msg = '%s\t%s' % (message, top.get_id_hash())
1166
1167         old_log = patch.get_log()
1168         if old_log:
1169             parents = [old_log]
1170         else:
1171             parents = []
1172
1173         log = git.commit(message = msg, parents = parents,
1174                          cache_update = False, tree_id = top.get_tree(),
1175                          allowempty = True)
1176         patch.set_log(log)
1177
1178     def hide_patch(self, name):
1179         """Add the patch to the hidden list.
1180         """
1181         if not self.patch_exists(name):
1182             raise StackException, 'Unknown patch "%s"' % name
1183         elif self.patch_hidden(name):
1184             raise StackException, 'Patch "%s" already hidden' % name
1185
1186         append_string(self.__hidden_file, name)
1187
1188     def unhide_patch(self, name):
1189         """Add the patch to the hidden list.
1190         """
1191         if not self.patch_exists(name):
1192             raise StackException, 'Unknown patch "%s"' % name
1193         hidden = self.get_hidden()
1194         if not name in hidden:
1195             raise StackException, 'Patch "%s" not hidden' % name
1196
1197         hidden.remove(name)
1198
1199         f = file(self.__hidden_file, 'w+')
1200         f.writelines([line + '\n' for line in hidden])
1201         f.close()