New stg command: assimilate
[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 def get_branch(self):
336 """Return the branch name for the Series object
337 """
338 return self.__name
339
340 def __set_current(self, name):
341 """Sets the topmost patch
342 """
343 if name:
344 write_string(self.__current_file, name)
345 else:
346 create_empty_file(self.__current_file)
347
348 def get_patch(self, name):
349 """Return a Patch object for the given name
350 """
351 return Patch(name, self.__patch_dir, self.__refs_dir)
352
353 def get_current_patch(self):
354 """Return a Patch object representing the topmost patch, or
355 None if there is no such patch."""
356 crt = self.get_current()
357 if not crt:
358 return None
359 return Patch(crt, self.__patch_dir, self.__refs_dir)
360
361 def get_current(self):
362 """Return the name of the topmost patch, or None if there is
363 no such patch."""
364 if os.path.isfile(self.__current_file):
365 name = read_string(self.__current_file)
366 else:
367 return None
368 if name == '':
369 return None
370 else:
371 return name
372
373 def get_applied(self):
374 if not os.path.isfile(self.__applied_file):
375 raise StackException, 'Branch "%s" not initialised' % self.__name
376 f = file(self.__applied_file)
377 names = [line.strip() for line in f.readlines()]
378 f.close()
379 return names
380
381 def get_unapplied(self):
382 if not os.path.isfile(self.__unapplied_file):
383 raise StackException, 'Branch "%s" not initialised' % self.__name
384 f = file(self.__unapplied_file)
385 names = [line.strip() for line in f.readlines()]
386 f.close()
387 return names
388
389 def get_base_file(self):
390 self.__begin_stack_check()
391 return self.__base_file
392
393 def get_protected(self):
394 return os.path.isfile(os.path.join(self.__series_dir, 'protected'))
395
396 def protect(self):
397 protect_file = os.path.join(self.__series_dir, 'protected')
398 if not os.path.isfile(protect_file):
399 create_empty_file(protect_file)
400
401 def unprotect(self):
402 protect_file = os.path.join(self.__series_dir, 'protected')
403 if os.path.isfile(protect_file):
404 os.remove(protect_file)
405
406 def get_description(self):
407 if os.path.isfile(self.__descr_file):
408 return read_string(self.__descr_file)
409 else:
410 return ''
411
412 def __patch_is_current(self, patch):
413 return patch.get_name() == read_string(self.__current_file)
414
415 def __patch_applied(self, name):
416 """Return true if the patch exists in the applied list
417 """
418 return name in self.get_applied()
419
420 def __patch_unapplied(self, name):
421 """Return true if the patch exists in the unapplied list
422 """
423 return name in self.get_unapplied()
424
425 def patch_exists(self, name):
426 """Return true if there is a patch with the given name, false
427 otherwise."""
428 return self.__patch_applied(name) or self.__patch_applied(name)
429
430 def __begin_stack_check(self):
431 """Save the current HEAD into .git/refs/heads/base if the stack
432 is empty
433 """
434 if len(self.get_applied()) == 0:
435 head = git.get_head()
436 write_string(self.__base_file, head)
437
438 def __end_stack_check(self):
439 """Remove .git/refs/heads/base if the stack is empty.
440 This warning should never happen
441 """
442 if len(self.get_applied()) == 0 \
443 and read_string(self.__base_file) != git.get_head():
444 print 'Warning: stack empty but the HEAD and base are different'
445
446 def head_top_equal(self):
447 """Return true if the head and the top are the same
448 """
449 crt = self.get_current_patch()
450 if not crt:
451 # we don't care, no patches applied
452 return True
453 return git.get_head() == crt.get_top()
454
455 def is_initialised(self):
456 """Checks if series is already initialised
457 """
458 return os.path.isdir(self.__patch_dir)
459
460 def init(self, create_at=False):
461 """Initialises the stgit series
462 """
463 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
464
465 if os.path.exists(self.__patch_dir):
466 raise StackException, self.__patch_dir + ' already exists'
467 if os.path.exists(self.__refs_dir):
468 raise StackException, self.__refs_dir + ' already exists'
469 if os.path.exists(self.__base_file):
470 raise StackException, self.__base_file + ' already exists'
471
472 if (create_at!=False):
473 git.create_branch(self.__name, create_at)
474
475 os.makedirs(self.__patch_dir)
476
477 create_dirs(bases_dir)
478
479 create_empty_file(self.__applied_file)
480 create_empty_file(self.__unapplied_file)
481 create_empty_file(self.__descr_file)
482 os.makedirs(os.path.join(self.__series_dir, 'patches'))
483 os.makedirs(self.__refs_dir)
484 self.__begin_stack_check()
485
486 def convert(self):
487 """Either convert to use a separate patch directory, or
488 unconvert to place the patches in the same directory with
489 series control files
490 """
491 if self.__patch_dir == self.__series_dir:
492 print 'Converting old-style to new-style...',
493 sys.stdout.flush()
494
495 self.__patch_dir = os.path.join(self.__series_dir, 'patches')
496 os.makedirs(self.__patch_dir)
497
498 for p in self.get_applied() + self.get_unapplied():
499 src = os.path.join(self.__series_dir, p)
500 dest = os.path.join(self.__patch_dir, p)
501 os.rename(src, dest)
502
503 print 'done'
504
505 else:
506 print 'Converting new-style to old-style...',
507 sys.stdout.flush()
508
509 for p in self.get_applied() + self.get_unapplied():
510 src = os.path.join(self.__patch_dir, p)
511 dest = os.path.join(self.__series_dir, p)
512 os.rename(src, dest)
513
514 if not os.listdir(self.__patch_dir):
515 os.rmdir(self.__patch_dir)
516 print 'done'
517 else:
518 print 'Patch directory %s is not empty.' % self.__name
519
520 self.__patch_dir = self.__series_dir
521
522 def rename(self, to_name):
523 """Renames a series
524 """
525 to_stack = Series(to_name)
526
527 if to_stack.is_initialised():
528 raise StackException, '"%s" already exists' % to_stack.get_branch()
529 if os.path.exists(to_stack.__base_file):
530 os.remove(to_stack.__base_file)
531
532 git.rename_branch(self.__name, to_name)
533
534 if os.path.isdir(self.__series_dir):
535 rename(os.path.join(self.__base_dir, 'patches'),
536 self.__name, to_stack.__name)
537 if os.path.exists(self.__base_file):
538 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
539 self.__name, to_stack.__name)
540 if os.path.exists(self.__refs_dir):
541 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
542 self.__name, to_stack.__name)
543
544 self.__init__(to_name)
545
546 def clone(self, target_series):
547 """Clones a series
548 """
549 base = read_string(self.get_base_file())
550 Series(target_series).init(create_at = base)
551 new_series = Series(target_series)
552
553 # generate an artificial description file
554 write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
555
556 # clone self's entire series as unapplied patches
557 patches = self.get_applied() + self.get_unapplied()
558 patches.reverse()
559 for p in patches:
560 patch = self.get_patch(p)
561 new_series.new_patch(p, message = patch.get_description(),
562 can_edit = False, unapplied = True,
563 bottom = patch.get_bottom(),
564 top = patch.get_top(),
565 author_name = patch.get_authname(),
566 author_email = patch.get_authemail(),
567 author_date = patch.get_authdate())
568
569 # fast forward the cloned series to self's top
570 new_series.forward_patches(self.get_applied())
571
572 def delete(self, force = False):
573 """Deletes an stgit series
574 """
575 if self.is_initialised():
576 patches = self.get_unapplied() + self.get_applied()
577 if not force and patches:
578 raise StackException, \
579 'Cannot delete: the series still contains patches'
580 for p in patches:
581 Patch(p, self.__patch_dir, self.__refs_dir).delete()
582
583 if os.path.exists(self.__applied_file):
584 os.remove(self.__applied_file)
585 if os.path.exists(self.__unapplied_file):
586 os.remove(self.__unapplied_file)
587 if os.path.exists(self.__current_file):
588 os.remove(self.__current_file)
589 if os.path.exists(self.__descr_file):
590 os.remove(self.__descr_file)
591 if not os.listdir(self.__patch_dir):
592 os.rmdir(self.__patch_dir)
593 else:
594 print 'Patch directory %s is not empty.' % self.__name
595 if not os.listdir(self.__series_dir):
596 remove_dirs(os.path.join(self.__base_dir, 'patches'),
597 self.__name)
598 else:
599 print 'Series directory %s is not empty.' % self.__name
600 if not os.listdir(self.__refs_dir):
601 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
602 self.__name)
603 else:
604 print 'Refs directory %s is not empty.' % self.__refs_dir
605
606 if os.path.exists(self.__base_file):
607 remove_file_and_dirs(
608 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
609
610 def refresh_patch(self, files = None, message = None, edit = False,
611 show_patch = False,
612 cache_update = True,
613 author_name = None, author_email = None,
614 author_date = None,
615 committer_name = None, committer_email = None,
616 backup = False, sign_str = None, log = 'refresh'):
617 """Generates a new commit for the given patch
618 """
619 name = self.get_current()
620 if not name:
621 raise StackException, 'No patches applied'
622
623 patch = Patch(name, self.__patch_dir, self.__refs_dir)
624
625 descr = patch.get_description()
626 if not (message or descr):
627 edit = True
628 descr = ''
629 elif message:
630 descr = message
631
632 if not message and edit:
633 descr = edit_file(self, descr.rstrip(), \
634 'Please edit the description for patch "%s" ' \
635 'above.' % name, show_patch)
636
637 if not author_name:
638 author_name = patch.get_authname()
639 if not author_email:
640 author_email = patch.get_authemail()
641 if not author_date:
642 author_date = patch.get_authdate()
643 if not committer_name:
644 committer_name = patch.get_commname()
645 if not committer_email:
646 committer_email = patch.get_commemail()
647
648 if sign_str:
649 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
650 committer_name, committer_email)
651
652 bottom = patch.get_bottom()
653
654 commit_id = git.commit(files = files,
655 message = descr, parents = [bottom],
656 cache_update = cache_update,
657 allowempty = True,
658 author_name = author_name,
659 author_email = author_email,
660 author_date = author_date,
661 committer_name = committer_name,
662 committer_email = committer_email)
663
664 patch.set_bottom(bottom, backup = backup)
665 patch.set_top(commit_id, backup = backup)
666 patch.set_description(descr)
667 patch.set_authname(author_name)
668 patch.set_authemail(author_email)
669 patch.set_authdate(author_date)
670 patch.set_commname(committer_name)
671 patch.set_commemail(committer_email)
672
673 if log:
674 self.log_patch(patch, log)
675
676 return commit_id
677
678 def undo_refresh(self):
679 """Undo the patch boundaries changes caused by 'refresh'
680 """
681 name = self.get_current()
682 assert(name)
683
684 patch = Patch(name, self.__patch_dir, self.__refs_dir)
685 old_bottom = patch.get_old_bottom()
686 old_top = patch.get_old_top()
687
688 # the bottom of the patch is not changed by refresh. If the
689 # old_bottom is different, there wasn't any previous 'refresh'
690 # command (probably only a 'push')
691 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
692 raise StackException, 'No refresh undo information available'
693
694 git.reset(tree_id = old_top, check_out = False)
695 if patch.restore_old_boundaries():
696 self.log_patch(patch, 'undo')
697
698 def new_patch(self, name, message = None, can_edit = True,
699 unapplied = False, show_patch = False,
700 top = None, bottom = None,
701 author_name = None, author_email = None, author_date = None,
702 committer_name = None, committer_email = None,
703 before_existing = False, refresh = True):
704 """Creates a new patch
705 """
706 if self.__patch_applied(name) or self.__patch_unapplied(name):
707 raise StackException, 'Patch "%s" already exists' % name
708
709 if not message and can_edit:
710 descr = edit_file(self, None, \
711 'Please enter the description for patch "%s" ' \
712 'above.' % name, show_patch)
713 else:
714 descr = message
715
716 head = git.get_head()
717
718 self.__begin_stack_check()
719
720 patch = Patch(name, self.__patch_dir, self.__refs_dir)
721 patch.create()
722
723 if bottom:
724 patch.set_bottom(bottom)
725 else:
726 patch.set_bottom(head)
727 if top:
728 patch.set_top(top)
729 else:
730 patch.set_top(head)
731
732 patch.set_description(descr)
733 patch.set_authname(author_name)
734 patch.set_authemail(author_email)
735 patch.set_authdate(author_date)
736 patch.set_commname(committer_name)
737 patch.set_commemail(committer_email)
738
739 if unapplied:
740 self.log_patch(patch, 'new')
741
742 patches = [patch.get_name()] + self.get_unapplied()
743
744 f = file(self.__unapplied_file, 'w+')
745 f.writelines([line + '\n' for line in patches])
746 f.close()
747 elif before_existing:
748 self.log_patch(patch, 'new')
749
750 insert_string(self.__applied_file, patch.get_name())
751 if not self.get_current():
752 self.__set_current(name)
753 else:
754 append_string(self.__applied_file, patch.get_name())
755 self.__set_current(name)
756 if refresh:
757 self.refresh_patch(cache_update = False, log = 'new')
758
759 def delete_patch(self, name):
760 """Deletes a patch
761 """
762 patch = Patch(name, self.__patch_dir, self.__refs_dir)
763
764 if self.__patch_is_current(patch):
765 self.pop_patch(name)
766 elif self.__patch_applied(name):
767 raise StackException, 'Cannot remove an applied patch, "%s", ' \
768 'which is not current' % name
769 elif not name in self.get_unapplied():
770 raise StackException, 'Unknown patch "%s"' % name
771
772 patch.delete()
773
774 unapplied = self.get_unapplied()
775 unapplied.remove(name)
776 f = file(self.__unapplied_file, 'w+')
777 f.writelines([line + '\n' for line in unapplied])
778 f.close()
779 self.__begin_stack_check()
780
781 def forward_patches(self, names):
782 """Try to fast-forward an array of patches.
783
784 On return, patches in names[0:returned_value] have been pushed on the
785 stack. Apply the rest with push_patch
786 """
787 unapplied = self.get_unapplied()
788 self.__begin_stack_check()
789
790 forwarded = 0
791 top = git.get_head()
792
793 for name in names:
794 assert(name in unapplied)
795
796 patch = Patch(name, self.__patch_dir, self.__refs_dir)
797
798 head = top
799 bottom = patch.get_bottom()
800 top = patch.get_top()
801
802 # top != bottom always since we have a commit for each patch
803 if head == bottom:
804 # reset the backup information. No logging since the
805 # patch hasn't changed
806 patch.set_bottom(head, backup = True)
807 patch.set_top(top, backup = True)
808
809 else:
810 head_tree = git.get_commit(head).get_tree()
811 bottom_tree = git.get_commit(bottom).get_tree()
812 if head_tree == bottom_tree:
813 # We must just reparent this patch and create a new commit
814 # for it
815 descr = patch.get_description()
816 author_name = patch.get_authname()
817 author_email = patch.get_authemail()
818 author_date = patch.get_authdate()
819 committer_name = patch.get_commname()
820 committer_email = patch.get_commemail()
821
822 top_tree = git.get_commit(top).get_tree()
823
824 top = git.commit(message = descr, parents = [head],
825 cache_update = False,
826 tree_id = top_tree,
827 allowempty = True,
828 author_name = author_name,
829 author_email = author_email,
830 author_date = author_date,
831 committer_name = committer_name,
832 committer_email = committer_email)
833
834 patch.set_bottom(head, backup = True)
835 patch.set_top(top, backup = True)
836
837 self.log_patch(patch, 'push(f)')
838 else:
839 top = head
840 # stop the fast-forwarding, must do a real merge
841 break
842
843 forwarded+=1
844 unapplied.remove(name)
845
846 if forwarded == 0:
847 return 0
848
849 git.switch(top)
850
851 append_strings(self.__applied_file, names[0:forwarded])
852
853 f = file(self.__unapplied_file, 'w+')
854 f.writelines([line + '\n' for line in unapplied])
855 f.close()
856
857 self.__set_current(name)
858
859 return forwarded
860
861 def merged_patches(self, names):
862 """Test which patches were merged upstream by reverse-applying
863 them in reverse order. The function returns the list of
864 patches detected to have been applied. The state of the tree
865 is restored to the original one
866 """
867 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
868 for name in names]
869 patches.reverse()
870
871 merged = []
872 for p in patches:
873 if git.apply_diff(p.get_top(), p.get_bottom()):
874 merged.append(p.get_name())
875 merged.reverse()
876
877 git.reset()
878
879 return merged
880
881 def push_patch(self, name, empty = False):
882 """Pushes a patch on the stack
883 """
884 unapplied = self.get_unapplied()
885 assert(name in unapplied)
886
887 self.__begin_stack_check()
888
889 patch = Patch(name, self.__patch_dir, self.__refs_dir)
890
891 head = git.get_head()
892 bottom = patch.get_bottom()
893 top = patch.get_top()
894
895 ex = None
896 modified = False
897
898 # top != bottom always since we have a commit for each patch
899 if empty:
900 # just make an empty patch (top = bottom = HEAD). This
901 # option is useful to allow undoing already merged
902 # patches. The top is updated by refresh_patch since we
903 # need an empty commit
904 patch.set_bottom(head, backup = True)
905 patch.set_top(head, backup = True)
906 modified = True
907 elif head == bottom:
908 # reset the backup information. No need for logging
909 patch.set_bottom(bottom, backup = True)
910 patch.set_top(top, backup = True)
911
912 git.switch(top)
913 else:
914 # new patch needs to be refreshed.
915 # The current patch is empty after merge.
916 patch.set_bottom(head, backup = True)
917 patch.set_top(head, backup = True)
918
919 # Try the fast applying first. If this fails, fall back to the
920 # three-way merge
921 if not git.apply_diff(bottom, top):
922 # if git.apply_diff() fails, the patch requires a diff3
923 # merge and can be reported as modified
924 modified = True
925
926 # merge can fail but the patch needs to be pushed
927 try:
928 git.merge(bottom, head, top)
929 except git.GitException, ex:
930 print >> sys.stderr, \
931 'The merge failed during "push". ' \
932 'Use "refresh" after fixing the conflicts'
933
934 append_string(self.__applied_file, name)
935
936 unapplied.remove(name)
937 f = file(self.__unapplied_file, 'w+')
938 f.writelines([line + '\n' for line in unapplied])
939 f.close()
940
941 self.__set_current(name)
942
943 # head == bottom case doesn't need to refresh the patch
944 if empty or head != bottom:
945 if not ex:
946 # if the merge was OK and no conflicts, just refresh the patch
947 # The GIT cache was already updated by the merge operation
948 if modified:
949 log = 'push(m)'
950 else:
951 log = 'push'
952 self.refresh_patch(cache_update = False, log = log)
953 else:
954 raise StackException, str(ex)
955
956 return modified
957
958 def undo_push(self):
959 name = self.get_current()
960 assert(name)
961
962 patch = Patch(name, self.__patch_dir, self.__refs_dir)
963 old_bottom = patch.get_old_bottom()
964 old_top = patch.get_old_top()
965
966 # the top of the patch is changed by a push operation only
967 # together with the bottom (otherwise the top was probably
968 # modified by 'refresh'). If they are both unchanged, there
969 # was a fast forward
970 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
971 raise StackException, 'No push undo information available'
972
973 git.reset()
974 self.pop_patch(name)
975 ret = patch.restore_old_boundaries()
976 if ret:
977 self.log_patch(patch, 'undo')
978
979 return ret
980
981 def pop_patch(self, name, keep = False):
982 """Pops the top patch from the stack
983 """
984 applied = self.get_applied()
985 applied.reverse()
986 assert(name in applied)
987
988 patch = Patch(name, self.__patch_dir, self.__refs_dir)
989
990 # only keep the local changes
991 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
992 raise StackException, \
993 'Failed to pop patches while preserving the local changes'
994
995 git.switch(patch.get_bottom(), keep)
996
997 # save the new applied list
998 idx = applied.index(name) + 1
999
1000 popped = applied[:idx]
1001 popped.reverse()
1002 unapplied = popped + self.get_unapplied()
1003
1004 f = file(self.__unapplied_file, 'w+')
1005 f.writelines([line + '\n' for line in unapplied])
1006 f.close()
1007
1008 del applied[:idx]
1009 applied.reverse()
1010
1011 f = file(self.__applied_file, 'w+')
1012 f.writelines([line + '\n' for line in applied])
1013 f.close()
1014
1015 if applied == []:
1016 self.__set_current(None)
1017 else:
1018 self.__set_current(applied[-1])
1019
1020 self.__end_stack_check()
1021
1022 def empty_patch(self, name):
1023 """Returns True if the patch is empty
1024 """
1025 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1026 bottom = patch.get_bottom()
1027 top = patch.get_top()
1028
1029 if bottom == top:
1030 return True
1031 elif git.get_commit(top).get_tree() \
1032 == git.get_commit(bottom).get_tree():
1033 return True
1034
1035 return False
1036
1037 def rename_patch(self, oldname, newname):
1038 applied = self.get_applied()
1039 unapplied = self.get_unapplied()
1040
1041 if oldname == newname:
1042 raise StackException, '"To" name and "from" name are the same'
1043
1044 if newname in applied or newname in unapplied:
1045 raise StackException, 'Patch "%s" already exists' % newname
1046
1047 if oldname in unapplied:
1048 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1049 unapplied[unapplied.index(oldname)] = newname
1050
1051 f = file(self.__unapplied_file, 'w+')
1052 f.writelines([line + '\n' for line in unapplied])
1053 f.close()
1054 elif oldname in applied:
1055 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1056 if oldname == self.get_current():
1057 self.__set_current(newname)
1058
1059 applied[applied.index(oldname)] = newname
1060
1061 f = file(self.__applied_file, 'w+')
1062 f.writelines([line + '\n' for line in applied])
1063 f.close()
1064 else:
1065 raise StackException, 'Unknown patch "%s"' % oldname
1066
1067 def log_patch(self, patch, message):
1068 """Generate a log commit for a patch
1069 """
1070 top = git.get_commit(patch.get_top())
1071 msg = '%s\t%s' % (message, top.get_id_hash())
1072
1073 old_log = patch.get_log()
1074 if old_log:
1075 parents = [old_log]
1076 else:
1077 parents = []
1078
1079 log = git.commit(message = msg, parents = parents,
1080 cache_update = False, tree_id = top.get_tree(),
1081 allowempty = True)
1082 patch.set_log(log)