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