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