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