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