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