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