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