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