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