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