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