chiark / gitweb /
dc6caa6d6820db55afb27c5a194b1806ec70141f
[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         bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
522
523         if os.path.exists(self.__patch_dir):
524             raise StackException, self.__patch_dir + ' already exists'
525         if os.path.exists(self.__refs_dir):
526             raise StackException, self.__refs_dir + ' already exists'
527         if os.path.exists(self.__base_file):
528             raise StackException, self.__base_file + ' already exists'
529
530         if (create_at!=False):
531             git.create_branch(self.__name, create_at)
532
533         os.makedirs(self.__patch_dir)
534
535         self.set_parent(parent_remote, parent_branch)
536         
537         create_dirs(bases_dir)
538
539         self.create_empty_field('applied')
540         self.create_empty_field('unapplied')
541         self.create_empty_field('description')
542         os.makedirs(os.path.join(self._dir(), 'patches'))
543         os.makedirs(self.__refs_dir)
544         self.__begin_stack_check()
545
546     def convert(self):
547         """Either convert to use a separate patch directory, or
548         unconvert to place the patches in the same directory with
549         series control files
550         """
551         if self.__patch_dir == self._dir():
552             print 'Converting old-style to new-style...',
553             sys.stdout.flush()
554
555             self.__patch_dir = os.path.join(self._dir(), 'patches')
556             os.makedirs(self.__patch_dir)
557
558             for p in self.get_applied() + self.get_unapplied():
559                 src = os.path.join(self._dir(), p)
560                 dest = os.path.join(self.__patch_dir, p)
561                 os.rename(src, dest)
562
563             print 'done'
564
565         else:
566             print 'Converting new-style to old-style...',
567             sys.stdout.flush()
568
569             for p in self.get_applied() + self.get_unapplied():
570                 src = os.path.join(self.__patch_dir, p)
571                 dest = os.path.join(self._dir(), p)
572                 os.rename(src, dest)
573
574             if not os.listdir(self.__patch_dir):
575                 os.rmdir(self.__patch_dir)
576                 print 'done'
577             else:
578                 print 'Patch directory %s is not empty.' % self.__name
579
580             self.__patch_dir = self._dir()
581
582     def rename(self, to_name):
583         """Renames a series
584         """
585         to_stack = Series(to_name)
586
587         if to_stack.is_initialised():
588             raise StackException, '"%s" already exists' % to_stack.get_branch()
589         if os.path.exists(to_stack.__base_file):
590             os.remove(to_stack.__base_file)
591
592         git.rename_branch(self.__name, to_name)
593
594         if os.path.isdir(self._dir()):
595             rename(os.path.join(self.__base_dir, 'patches'),
596                    self.__name, to_stack.__name)
597         if os.path.exists(self.__base_file):
598             rename(os.path.join(self.__base_dir, 'refs', 'bases'),
599                    self.__name, to_stack.__name)
600         if os.path.exists(self.__refs_dir):
601             rename(os.path.join(self.__base_dir, 'refs', 'patches'),
602                    self.__name, to_stack.__name)
603
604         # Rename the config section
605         config.rename_section("branch.%s" % self.__name,
606                               "branch.%s" % to_name)
607
608         self.__init__(to_name)
609
610     def clone(self, target_series):
611         """Clones a series
612         """
613         try:
614             # allow cloning of branches not under StGIT control
615             base = self.get_base()
616         except:
617             base = git.get_head()
618         Series(target_series).init(create_at = base)
619         new_series = Series(target_series)
620
621         # generate an artificial description file
622         new_series.set_description('clone of "%s"' % self.__name)
623
624         # clone self's entire series as unapplied patches
625         try:
626             # allow cloning of branches not under StGIT control
627             applied = self.get_applied()
628             unapplied = self.get_unapplied()
629             patches = applied + unapplied
630             patches.reverse()
631         except:
632             patches = applied = unapplied = []
633         for p in patches:
634             patch = self.get_patch(p)
635             new_series.new_patch(p, message = patch.get_description(),
636                                  can_edit = False, unapplied = True,
637                                  bottom = patch.get_bottom(),
638                                  top = patch.get_top(),
639                                  author_name = patch.get_authname(),
640                                  author_email = patch.get_authemail(),
641                                  author_date = patch.get_authdate())
642
643         # fast forward the cloned series to self's top
644         new_series.forward_patches(applied)
645
646         # Clone remote and merge settings
647         value = config.get('branch.%s.remote' % self.__name)
648         if value:
649             config.set('branch.%s.remote' % target_series, value)
650
651         value = config.get('branch.%s.merge' % self.__name)
652         if value:
653             config.set('branch.%s.merge' % target_series, value)
654
655     def delete(self, force = False):
656         """Deletes an stgit series
657         """
658         if self.is_initialised():
659             patches = self.get_unapplied() + self.get_applied()
660             if not force and patches:
661                 raise StackException, \
662                       'Cannot delete: the series still contains patches'
663             for p in patches:
664                 Patch(p, self.__patch_dir, self.__refs_dir).delete()
665
666             # remove the trash directory
667             for fname in os.listdir(self.__trash_dir):
668                 os.remove(fname)
669             os.rmdir(self.__trash_dir)
670
671             # FIXME: find a way to get rid of those manual removals
672             # (move functionality to StgitObject ?)
673             if os.path.exists(self.__applied_file):
674                 os.remove(self.__applied_file)
675             if os.path.exists(self.__unapplied_file):
676                 os.remove(self.__unapplied_file)
677             if os.path.exists(self.__hidden_file):
678                 os.remove(self.__hidden_file)
679             if os.path.exists(self.__current_file):
680                 os.remove(self.__current_file)
681             if os.path.exists(self.__descr_file):
682                 os.remove(self.__descr_file)
683             if not os.listdir(self.__patch_dir):
684                 os.rmdir(self.__patch_dir)
685             else:
686                 print 'Patch directory %s is not empty.' % self.__name
687             if not os.listdir(self._dir()):
688                 remove_dirs(os.path.join(self.__base_dir, 'patches'),
689                             self.__name)
690             else:
691                 print 'Series directory %s is not empty.' % self.__name
692             if not os.listdir(self.__refs_dir):
693                 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
694                             self.__name)
695             else:
696                 print 'Refs directory %s is not empty.' % self.__refs_dir
697
698         if os.path.exists(self.__base_file):
699             remove_file_and_dirs(
700                 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
701
702     def refresh_patch(self, files = None, message = None, edit = False,
703                       show_patch = False,
704                       cache_update = True,
705                       author_name = None, author_email = None,
706                       author_date = None,
707                       committer_name = None, committer_email = None,
708                       backup = False, sign_str = None, log = 'refresh'):
709         """Generates a new commit for the given patch
710         """
711         name = self.get_current()
712         if not name:
713             raise StackException, 'No patches applied'
714
715         patch = Patch(name, self.__patch_dir, self.__refs_dir)
716
717         descr = patch.get_description()
718         if not (message or descr):
719             edit = True
720             descr = ''
721         elif message:
722             descr = message
723
724         if not message and edit:
725             descr = edit_file(self, descr.rstrip(), \
726                               'Please edit the description for patch "%s" ' \
727                               'above.' % name, show_patch)
728
729         if not author_name:
730             author_name = patch.get_authname()
731         if not author_email:
732             author_email = patch.get_authemail()
733         if not author_date:
734             author_date = patch.get_authdate()
735         if not committer_name:
736             committer_name = patch.get_commname()
737         if not committer_email:
738             committer_email = patch.get_commemail()
739
740         if sign_str:
741             descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
742                                            committer_name, committer_email)
743
744         bottom = patch.get_bottom()
745
746         commit_id = git.commit(files = files,
747                                message = descr, parents = [bottom],
748                                cache_update = cache_update,
749                                allowempty = True,
750                                author_name = author_name,
751                                author_email = author_email,
752                                author_date = author_date,
753                                committer_name = committer_name,
754                                committer_email = committer_email)
755
756         patch.set_bottom(bottom, backup = backup)
757         patch.set_top(commit_id, backup = backup)
758         patch.set_description(descr)
759         patch.set_authname(author_name)
760         patch.set_authemail(author_email)
761         patch.set_authdate(author_date)
762         patch.set_commname(committer_name)
763         patch.set_commemail(committer_email)
764
765         if log:
766             self.log_patch(patch, log)
767
768         return commit_id
769
770     def undo_refresh(self):
771         """Undo the patch boundaries changes caused by 'refresh'
772         """
773         name = self.get_current()
774         assert(name)
775
776         patch = Patch(name, self.__patch_dir, self.__refs_dir)
777         old_bottom = patch.get_old_bottom()
778         old_top = patch.get_old_top()
779
780         # the bottom of the patch is not changed by refresh. If the
781         # old_bottom is different, there wasn't any previous 'refresh'
782         # command (probably only a 'push')
783         if old_bottom != patch.get_bottom() or old_top == patch.get_top():
784             raise StackException, 'No undo information available'
785
786         git.reset(tree_id = old_top, check_out = False)
787         if patch.restore_old_boundaries():
788             self.log_patch(patch, 'undo')
789
790     def new_patch(self, name, message = None, can_edit = True,
791                   unapplied = False, show_patch = False,
792                   top = None, bottom = None,
793                   author_name = None, author_email = None, author_date = None,
794                   committer_name = None, committer_email = None,
795                   before_existing = False, refresh = True):
796         """Creates a new patch
797         """
798         self.__patch_name_valid(name)
799
800         if self.patch_applied(name) or self.patch_unapplied(name):
801             raise StackException, 'Patch "%s" already exists' % name
802
803         if not message and can_edit:
804             descr = edit_file(self, None, \
805                               'Please enter the description for patch "%s" ' \
806                               'above.' % name, show_patch)
807         else:
808             descr = message
809
810         head = git.get_head()
811
812         self.__begin_stack_check()
813
814         patch = Patch(name, self.__patch_dir, self.__refs_dir)
815         patch.create()
816
817         if bottom:
818             patch.set_bottom(bottom)
819         else:
820             patch.set_bottom(head)
821         if top:
822             patch.set_top(top)
823         else:
824             patch.set_top(head)
825
826         patch.set_description(descr)
827         patch.set_authname(author_name)
828         patch.set_authemail(author_email)
829         patch.set_authdate(author_date)
830         patch.set_commname(committer_name)
831         patch.set_commemail(committer_email)
832
833         if unapplied:
834             self.log_patch(patch, 'new')
835
836             patches = [patch.get_name()] + self.get_unapplied()
837
838             f = file(self.__unapplied_file, 'w+')
839             f.writelines([line + '\n' for line in patches])
840             f.close()
841         elif before_existing:
842             self.log_patch(patch, 'new')
843
844             insert_string(self.__applied_file, patch.get_name())
845             if not self.get_current():
846                 self.__set_current(name)
847         else:
848             append_string(self.__applied_file, patch.get_name())
849             self.__set_current(name)
850             if refresh:
851                 self.refresh_patch(cache_update = False, log = 'new')
852
853     def delete_patch(self, name):
854         """Deletes a patch
855         """
856         self.__patch_name_valid(name)
857         patch = Patch(name, self.__patch_dir, self.__refs_dir)
858
859         if self.__patch_is_current(patch):
860             self.pop_patch(name)
861         elif self.patch_applied(name):
862             raise StackException, 'Cannot remove an applied patch, "%s", ' \
863                   'which is not current' % name
864         elif not name in self.get_unapplied():
865             raise StackException, 'Unknown patch "%s"' % name
866
867         # save the commit id to a trash file
868         write_string(os.path.join(self.__trash_dir, name), patch.get_top())
869
870         patch.delete()
871
872         unapplied = self.get_unapplied()
873         unapplied.remove(name)
874         f = file(self.__unapplied_file, 'w+')
875         f.writelines([line + '\n' for line in unapplied])
876         f.close()
877
878         if self.patch_hidden(name):
879             self.unhide_patch(name)
880
881         self.__begin_stack_check()
882
883     def forward_patches(self, names):
884         """Try to fast-forward an array of patches.
885
886         On return, patches in names[0:returned_value] have been pushed on the
887         stack. Apply the rest with push_patch
888         """
889         unapplied = self.get_unapplied()
890         self.__begin_stack_check()
891
892         forwarded = 0
893         top = git.get_head()
894
895         for name in names:
896             assert(name in unapplied)
897
898             patch = Patch(name, self.__patch_dir, self.__refs_dir)
899
900             head = top
901             bottom = patch.get_bottom()
902             top = patch.get_top()
903
904             # top != bottom always since we have a commit for each patch
905             if head == bottom:
906                 # reset the backup information. No logging since the
907                 # patch hasn't changed
908                 patch.set_bottom(head, backup = True)
909                 patch.set_top(top, backup = True)
910
911             else:
912                 head_tree = git.get_commit(head).get_tree()
913                 bottom_tree = git.get_commit(bottom).get_tree()
914                 if head_tree == bottom_tree:
915                     # We must just reparent this patch and create a new commit
916                     # for it
917                     descr = patch.get_description()
918                     author_name = patch.get_authname()
919                     author_email = patch.get_authemail()
920                     author_date = patch.get_authdate()
921                     committer_name = patch.get_commname()
922                     committer_email = patch.get_commemail()
923
924                     top_tree = git.get_commit(top).get_tree()
925
926                     top = git.commit(message = descr, parents = [head],
927                                      cache_update = False,
928                                      tree_id = top_tree,
929                                      allowempty = True,
930                                      author_name = author_name,
931                                      author_email = author_email,
932                                      author_date = author_date,
933                                      committer_name = committer_name,
934                                      committer_email = committer_email)
935
936                     patch.set_bottom(head, backup = True)
937                     patch.set_top(top, backup = True)
938
939                     self.log_patch(patch, 'push(f)')
940                 else:
941                     top = head
942                     # stop the fast-forwarding, must do a real merge
943                     break
944
945             forwarded+=1
946             unapplied.remove(name)
947
948         if forwarded == 0:
949             return 0
950
951         git.switch(top)
952
953         append_strings(self.__applied_file, names[0:forwarded])
954
955         f = file(self.__unapplied_file, 'w+')
956         f.writelines([line + '\n' for line in unapplied])
957         f.close()
958
959         self.__set_current(name)
960
961         return forwarded
962
963     def merged_patches(self, names):
964         """Test which patches were merged upstream by reverse-applying
965         them in reverse order. The function returns the list of
966         patches detected to have been applied. The state of the tree
967         is restored to the original one
968         """
969         patches = [Patch(name, self.__patch_dir, self.__refs_dir)
970                    for name in names]
971         patches.reverse()
972
973         merged = []
974         for p in patches:
975             if git.apply_diff(p.get_top(), p.get_bottom()):
976                 merged.append(p.get_name())
977         merged.reverse()
978
979         git.reset()
980
981         return merged
982
983     def push_patch(self, name, empty = False):
984         """Pushes a patch on the stack
985         """
986         unapplied = self.get_unapplied()
987         assert(name in unapplied)
988
989         self.__begin_stack_check()
990
991         patch = Patch(name, self.__patch_dir, self.__refs_dir)
992
993         head = git.get_head()
994         bottom = patch.get_bottom()
995         top = patch.get_top()
996
997         ex = None
998         modified = False
999
1000         # top != bottom always since we have a commit for each patch
1001         if empty:
1002             # just make an empty patch (top = bottom = HEAD). This
1003             # option is useful to allow undoing already merged
1004             # patches. The top is updated by refresh_patch since we
1005             # need an empty commit
1006             patch.set_bottom(head, backup = True)
1007             patch.set_top(head, backup = True)
1008             modified = True
1009         elif head == bottom:
1010             # reset the backup information. No need for logging
1011             patch.set_bottom(bottom, backup = True)
1012             patch.set_top(top, backup = True)
1013
1014             git.switch(top)
1015         else:
1016             # new patch needs to be refreshed.
1017             # The current patch is empty after merge.
1018             patch.set_bottom(head, backup = True)
1019             patch.set_top(head, backup = True)
1020
1021             # Try the fast applying first. If this fails, fall back to the
1022             # three-way merge
1023             if not git.apply_diff(bottom, top):
1024                 # if git.apply_diff() fails, the patch requires a diff3
1025                 # merge and can be reported as modified
1026                 modified = True
1027
1028                 # merge can fail but the patch needs to be pushed
1029                 try:
1030                     git.merge(bottom, head, top, recursive = True)
1031                 except git.GitException, ex:
1032                     print >> sys.stderr, \
1033                           'The merge failed during "push". ' \
1034                           'Use "refresh" after fixing the conflicts'
1035
1036         append_string(self.__applied_file, name)
1037
1038         unapplied.remove(name)
1039         f = file(self.__unapplied_file, 'w+')
1040         f.writelines([line + '\n' for line in unapplied])
1041         f.close()
1042
1043         self.__set_current(name)
1044
1045         # head == bottom case doesn't need to refresh the patch
1046         if empty or head != bottom:
1047             if not ex:
1048                 # if the merge was OK and no conflicts, just refresh the patch
1049                 # The GIT cache was already updated by the merge operation
1050                 if modified:
1051                     log = 'push(m)'
1052                 else:
1053                     log = 'push'
1054                 self.refresh_patch(cache_update = False, log = log)
1055             else:
1056                 # we store the correctly merged files only for
1057                 # tracking the conflict history. Note that the
1058                 # git.merge() operations should always leave the index
1059                 # in a valid state (i.e. only stage 0 files)
1060                 self.refresh_patch(cache_update = False, log = 'push(c)')
1061                 raise StackException, str(ex)
1062
1063         return modified
1064
1065     def undo_push(self):
1066         name = self.get_current()
1067         assert(name)
1068
1069         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1070         old_bottom = patch.get_old_bottom()
1071         old_top = patch.get_old_top()
1072
1073         # the top of the patch is changed by a push operation only
1074         # together with the bottom (otherwise the top was probably
1075         # modified by 'refresh'). If they are both unchanged, there
1076         # was a fast forward
1077         if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1078             raise StackException, 'No undo information available'
1079
1080         git.reset()
1081         self.pop_patch(name)
1082         ret = patch.restore_old_boundaries()
1083         if ret:
1084             self.log_patch(patch, 'undo')
1085
1086         return ret
1087
1088     def pop_patch(self, name, keep = False):
1089         """Pops the top patch from the stack
1090         """
1091         applied = self.get_applied()
1092         applied.reverse()
1093         assert(name in applied)
1094
1095         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1096
1097         # only keep the local changes
1098         if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1099             raise StackException, \
1100                   'Failed to pop patches while preserving the local changes'
1101
1102         git.switch(patch.get_bottom(), keep)
1103
1104         # save the new applied list
1105         idx = applied.index(name) + 1
1106
1107         popped = applied[:idx]
1108         popped.reverse()
1109         unapplied = popped + self.get_unapplied()
1110
1111         f = file(self.__unapplied_file, 'w+')
1112         f.writelines([line + '\n' for line in unapplied])
1113         f.close()
1114
1115         del applied[:idx]
1116         applied.reverse()
1117
1118         f = file(self.__applied_file, 'w+')
1119         f.writelines([line + '\n' for line in applied])
1120         f.close()
1121
1122         if applied == []:
1123             self.__set_current(None)
1124         else:
1125             self.__set_current(applied[-1])
1126
1127         self.__end_stack_check()
1128
1129     def empty_patch(self, name):
1130         """Returns True if the patch is empty
1131         """
1132         self.__patch_name_valid(name)
1133         patch = Patch(name, self.__patch_dir, self.__refs_dir)
1134         bottom = patch.get_bottom()
1135         top = patch.get_top()
1136
1137         if bottom == top:
1138             return True
1139         elif git.get_commit(top).get_tree() \
1140                  == git.get_commit(bottom).get_tree():
1141             return True
1142
1143         return False
1144
1145     def rename_patch(self, oldname, newname):
1146         self.__patch_name_valid(newname)
1147
1148         applied = self.get_applied()
1149         unapplied = self.get_unapplied()
1150
1151         if oldname == newname:
1152             raise StackException, '"To" name and "from" name are the same'
1153
1154         if newname in applied or newname in unapplied:
1155             raise StackException, 'Patch "%s" already exists' % newname
1156
1157         if self.patch_hidden(oldname):
1158             self.unhide_patch(oldname)
1159             self.hide_patch(newname)
1160
1161         if oldname in unapplied:
1162             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1163             unapplied[unapplied.index(oldname)] = newname
1164
1165             f = file(self.__unapplied_file, 'w+')
1166             f.writelines([line + '\n' for line in unapplied])
1167             f.close()
1168         elif oldname in applied:
1169             Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1170             if oldname == self.get_current():
1171                 self.__set_current(newname)
1172
1173             applied[applied.index(oldname)] = newname
1174
1175             f = file(self.__applied_file, 'w+')
1176             f.writelines([line + '\n' for line in applied])
1177             f.close()
1178         else:
1179             raise StackException, 'Unknown patch "%s"' % oldname
1180
1181     def log_patch(self, patch, message):
1182         """Generate a log commit for a patch
1183         """
1184         top = git.get_commit(patch.get_top())
1185         msg = '%s\t%s' % (message, top.get_id_hash())
1186
1187         old_log = patch.get_log()
1188         if old_log:
1189             parents = [old_log]
1190         else:
1191             parents = []
1192
1193         log = git.commit(message = msg, parents = parents,
1194                          cache_update = False, tree_id = top.get_tree(),
1195                          allowempty = True)
1196         patch.set_log(log)
1197
1198     def hide_patch(self, name):
1199         """Add the patch to the hidden list.
1200         """
1201         if not self.patch_exists(name):
1202             raise StackException, 'Unknown patch "%s"' % name
1203         elif self.patch_hidden(name):
1204             raise StackException, 'Patch "%s" already hidden' % name
1205
1206         append_string(self.__hidden_file, name)
1207
1208     def unhide_patch(self, name):
1209         """Add the patch to the hidden list.
1210         """
1211         if not self.patch_exists(name):
1212             raise StackException, 'Unknown patch "%s"' % name
1213         hidden = self.get_hidden()
1214         if not name in hidden:
1215             raise StackException, 'Patch "%s" not hidden' % name
1216
1217         hidden.remove(name)
1218
1219         f = file(self.__hidden_file, 'w+')
1220         f.writelines([line + '\n' for line in hidden])
1221         f.close()