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