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