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