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