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