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