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