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