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