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