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