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