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