Add support to hide and unhide patches
[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.__hidden_file = os.path.join(self._dir(), 'hidden')
311 self.__current_file = os.path.join(self._dir(), 'current')
312 self.__descr_file = os.path.join(self._dir(), 'description')
313
314 # where this series keeps its patches
315 self.__patch_dir = os.path.join(self._dir(), 'patches')
316 if not os.path.isdir(self.__patch_dir):
317 self.__patch_dir = self._dir()
318
319 # if no __refs_dir, create and populate it (upgrade old repositories)
320 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
321 os.makedirs(self.__refs_dir)
322 for patch in self.get_applied() + self.get_unapplied():
323 self.get_patch(patch).update_top_ref()
324
325 # trash directory
326 self.__trash_dir = os.path.join(self._dir(), 'trash')
327 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
328 os.makedirs(self.__trash_dir)
329
330 def get_branch(self):
331 """Return the branch name for the Series object
332 """
333 return self.__name
334
335 def __set_current(self, name):
336 """Sets the topmost patch
337 """
338 self._set_field('current', name)
339
340 def get_patch(self, name):
341 """Return a Patch object for the given name
342 """
343 return Patch(name, self.__patch_dir, self.__refs_dir)
344
345 def get_current_patch(self):
346 """Return a Patch object representing the topmost patch, or
347 None if there is no such patch."""
348 crt = self.get_current()
349 if not crt:
350 return None
351 return Patch(crt, self.__patch_dir, self.__refs_dir)
352
353 def get_current(self):
354 """Return the name of the topmost patch, or None if there is
355 no such patch."""
356 name = self._get_field('current')
357 if name == '':
358 return None
359 else:
360 return name
361
362 def get_applied(self):
363 if not os.path.isfile(self.__applied_file):
364 raise StackException, 'Branch "%s" not initialised' % self.__name
365 f = file(self.__applied_file)
366 names = [line.strip() for line in f.readlines()]
367 f.close()
368 return names
369
370 def get_unapplied(self):
371 if not os.path.isfile(self.__unapplied_file):
372 raise StackException, 'Branch "%s" not initialised' % self.__name
373 f = file(self.__unapplied_file)
374 names = [line.strip() for line in f.readlines()]
375 f.close()
376 return names
377
378 def get_hidden(self):
379 if not os.path.isfile(self.__hidden_file):
380 return []
381 f = file(self.__hidden_file)
382 names = [line.strip() for line in f.readlines()]
383 f.close()
384 return names
385
386 def get_base_file(self):
387 self.__begin_stack_check()
388 return self.__base_file
389
390 def get_protected(self):
391 return os.path.isfile(os.path.join(self._dir(), 'protected'))
392
393 def protect(self):
394 protect_file = os.path.join(self._dir(), 'protected')
395 if not os.path.isfile(protect_file):
396 create_empty_file(protect_file)
397
398 def unprotect(self):
399 protect_file = os.path.join(self._dir(), 'protected')
400 if os.path.isfile(protect_file):
401 os.remove(protect_file)
402
403 def get_description(self):
404 return self._get_field('description') or ''
405
406 def set_description(self, line):
407 self._set_field('description', line)
408
409 def __patch_is_current(self, patch):
410 return patch.get_name() == self.get_current()
411
412 def patch_applied(self, name):
413 """Return true if the patch exists in the applied list
414 """
415 return name in self.get_applied()
416
417 def patch_unapplied(self, name):
418 """Return true if the patch exists in the unapplied list
419 """
420 return name in self.get_unapplied()
421
422 def patch_hidden(self, name):
423 """Return true if the patch is hidden.
424 """
425 return name in self.get_hidden()
426
427 def patch_exists(self, name):
428 """Return true if there is a patch with the given name, false
429 otherwise."""
430 return self.patch_applied(name) or self.patch_unapplied(name)
431
432 def __begin_stack_check(self):
433 """Save the current HEAD into .git/refs/heads/base if the stack
434 is empty
435 """
436 if len(self.get_applied()) == 0:
437 head = git.get_head()
438 write_string(self.__base_file, head)
439
440 def __end_stack_check(self):
441 """Remove .git/refs/heads/base if the stack is empty.
442 This warning should never happen
443 """
444 if len(self.get_applied()) == 0 \
445 and read_string(self.__base_file) != git.get_head():
446 print 'Warning: stack empty but the HEAD and base are different'
447
448 def head_top_equal(self):
449 """Return true if the head and the top are the same
450 """
451 crt = self.get_current_patch()
452 if not crt:
453 # we don't care, no patches applied
454 return True
455 return git.get_head() == crt.get_top()
456
457 def is_initialised(self):
458 """Checks if series is already initialised
459 """
460 return os.path.isdir(self.__patch_dir)
461
462 def init(self, create_at=False):
463 """Initialises the stgit series
464 """
465 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
466
467 if os.path.exists(self.__patch_dir):
468 raise StackException, self.__patch_dir + ' already exists'
469 if os.path.exists(self.__refs_dir):
470 raise StackException, self.__refs_dir + ' already exists'
471 if os.path.exists(self.__base_file):
472 raise StackException, self.__base_file + ' already exists'
473
474 if (create_at!=False):
475 git.create_branch(self.__name, create_at)
476
477 os.makedirs(self.__patch_dir)
478
479 create_dirs(bases_dir)
480
481 self.create_empty_field('applied')
482 self.create_empty_field('unapplied')
483 self.create_empty_field('description')
484 os.makedirs(os.path.join(self._dir(), 'patches'))
485 os.makedirs(self.__refs_dir)
486 self.__begin_stack_check()
487
488 def convert(self):
489 """Either convert to use a separate patch directory, or
490 unconvert to place the patches in the same directory with
491 series control files
492 """
493 if self.__patch_dir == self._dir():
494 print 'Converting old-style to new-style...',
495 sys.stdout.flush()
496
497 self.__patch_dir = os.path.join(self._dir(), 'patches')
498 os.makedirs(self.__patch_dir)
499
500 for p in self.get_applied() + self.get_unapplied():
501 src = os.path.join(self._dir(), p)
502 dest = os.path.join(self.__patch_dir, p)
503 os.rename(src, dest)
504
505 print 'done'
506
507 else:
508 print 'Converting new-style to old-style...',
509 sys.stdout.flush()
510
511 for p in self.get_applied() + self.get_unapplied():
512 src = os.path.join(self.__patch_dir, p)
513 dest = os.path.join(self._dir(), p)
514 os.rename(src, dest)
515
516 if not os.listdir(self.__patch_dir):
517 os.rmdir(self.__patch_dir)
518 print 'done'
519 else:
520 print 'Patch directory %s is not empty.' % self.__name
521
522 self.__patch_dir = self._dir()
523
524 def rename(self, to_name):
525 """Renames a series
526 """
527 to_stack = Series(to_name)
528
529 if to_stack.is_initialised():
530 raise StackException, '"%s" already exists' % to_stack.get_branch()
531 if os.path.exists(to_stack.__base_file):
532 os.remove(to_stack.__base_file)
533
534 git.rename_branch(self.__name, to_name)
535
536 if os.path.isdir(self._dir()):
537 rename(os.path.join(self.__base_dir, 'patches'),
538 self.__name, to_stack.__name)
539 if os.path.exists(self.__base_file):
540 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
541 self.__name, to_stack.__name)
542 if os.path.exists(self.__refs_dir):
543 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
544 self.__name, to_stack.__name)
545
546 self.__init__(to_name)
547
548 def clone(self, target_series):
549 """Clones a series
550 """
551 try:
552 # allow cloning of branches not under StGIT control
553 base = read_string(self.get_base_file())
554 except:
555 base = git.get_head()
556 Series(target_series).init(create_at = base)
557 new_series = Series(target_series)
558
559 # generate an artificial description file
560 new_series.set_description('clone of "%s"' % self.__name)
561
562 # clone self's entire series as unapplied patches
563 try:
564 # allow cloning of branches not under StGIT control
565 applied = self.get_applied()
566 unapplied = self.get_unapplied()
567 patches = applied + unapplied
568 patches.reverse()
569 except:
570 patches = applied = unapplied = []
571 for p in patches:
572 patch = self.get_patch(p)
573 new_series.new_patch(p, message = patch.get_description(),
574 can_edit = False, unapplied = True,
575 bottom = patch.get_bottom(),
576 top = patch.get_top(),
577 author_name = patch.get_authname(),
578 author_email = patch.get_authemail(),
579 author_date = patch.get_authdate())
580
581 # fast forward the cloned series to self's top
582 new_series.forward_patches(applied)
583
584 def delete(self, force = False):
585 """Deletes an stgit series
586 """
587 if self.is_initialised():
588 patches = self.get_unapplied() + self.get_applied()
589 if not force and patches:
590 raise StackException, \
591 'Cannot delete: the series still contains patches'
592 for p in patches:
593 Patch(p, self.__patch_dir, self.__refs_dir).delete()
594
595 # remove the trash directory
596 for fname in os.listdir(self.__trash_dir):
597 os.remove(fname)
598 os.rmdir(self.__trash_dir)
599
600 # FIXME: find a way to get rid of those manual removals
601 # (move functionnality to StgitObject ?)
602 if os.path.exists(self.__applied_file):
603 os.remove(self.__applied_file)
604 if os.path.exists(self.__unapplied_file):
605 os.remove(self.__unapplied_file)
606 if os.path.exists(self.__hidden_file):
607 os.remove(self.__hidden_file)
608 if os.path.exists(self.__current_file):
609 os.remove(self.__current_file)
610 if os.path.exists(self.__descr_file):
611 os.remove(self.__descr_file)
612 if not os.listdir(self.__patch_dir):
613 os.rmdir(self.__patch_dir)
614 else:
615 print 'Patch directory %s is not empty.' % self.__name
616 if not os.listdir(self._dir()):
617 remove_dirs(os.path.join(self.__base_dir, 'patches'),
618 self.__name)
619 else:
620 print 'Series directory %s is not empty.' % self.__name
621 if not os.listdir(self.__refs_dir):
622 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
623 self.__name)
624 else:
625 print 'Refs directory %s is not empty.' % self.__refs_dir
626
627 if os.path.exists(self.__base_file):
628 remove_file_and_dirs(
629 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
630
631 def refresh_patch(self, files = None, message = None, edit = False,
632 show_patch = False,
633 cache_update = True,
634 author_name = None, author_email = None,
635 author_date = None,
636 committer_name = None, committer_email = None,
637 backup = False, sign_str = None, log = 'refresh'):
638 """Generates a new commit for the given patch
639 """
640 name = self.get_current()
641 if not name:
642 raise StackException, 'No patches applied'
643
644 patch = Patch(name, self.__patch_dir, self.__refs_dir)
645
646 descr = patch.get_description()
647 if not (message or descr):
648 edit = True
649 descr = ''
650 elif message:
651 descr = message
652
653 if not message and edit:
654 descr = edit_file(self, descr.rstrip(), \
655 'Please edit the description for patch "%s" ' \
656 'above.' % name, show_patch)
657
658 if not author_name:
659 author_name = patch.get_authname()
660 if not author_email:
661 author_email = patch.get_authemail()
662 if not author_date:
663 author_date = patch.get_authdate()
664 if not committer_name:
665 committer_name = patch.get_commname()
666 if not committer_email:
667 committer_email = patch.get_commemail()
668
669 if sign_str:
670 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
671 committer_name, committer_email)
672
673 bottom = patch.get_bottom()
674
675 commit_id = git.commit(files = files,
676 message = descr, parents = [bottom],
677 cache_update = cache_update,
678 allowempty = True,
679 author_name = author_name,
680 author_email = author_email,
681 author_date = author_date,
682 committer_name = committer_name,
683 committer_email = committer_email)
684
685 patch.set_bottom(bottom, backup = backup)
686 patch.set_top(commit_id, backup = backup)
687 patch.set_description(descr)
688 patch.set_authname(author_name)
689 patch.set_authemail(author_email)
690 patch.set_authdate(author_date)
691 patch.set_commname(committer_name)
692 patch.set_commemail(committer_email)
693
694 if log:
695 self.log_patch(patch, log)
696
697 return commit_id
698
699 def undo_refresh(self):
700 """Undo the patch boundaries changes caused by 'refresh'
701 """
702 name = self.get_current()
703 assert(name)
704
705 patch = Patch(name, self.__patch_dir, self.__refs_dir)
706 old_bottom = patch.get_old_bottom()
707 old_top = patch.get_old_top()
708
709 # the bottom of the patch is not changed by refresh. If the
710 # old_bottom is different, there wasn't any previous 'refresh'
711 # command (probably only a 'push')
712 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
713 raise StackException, 'No undo information available'
714
715 git.reset(tree_id = old_top, check_out = False)
716 if patch.restore_old_boundaries():
717 self.log_patch(patch, 'undo')
718
719 def new_patch(self, name, message = None, can_edit = True,
720 unapplied = False, show_patch = False,
721 top = None, bottom = None,
722 author_name = None, author_email = None, author_date = None,
723 committer_name = None, committer_email = None,
724 before_existing = False, refresh = True):
725 """Creates a new patch
726 """
727 if self.patch_applied(name) or self.patch_unapplied(name):
728 raise StackException, 'Patch "%s" already exists' % name
729
730 if not message and can_edit:
731 descr = edit_file(self, None, \
732 'Please enter the description for patch "%s" ' \
733 'above.' % name, show_patch)
734 else:
735 descr = message
736
737 head = git.get_head()
738
739 self.__begin_stack_check()
740
741 patch = Patch(name, self.__patch_dir, self.__refs_dir)
742 patch.create()
743
744 if bottom:
745 patch.set_bottom(bottom)
746 else:
747 patch.set_bottom(head)
748 if top:
749 patch.set_top(top)
750 else:
751 patch.set_top(head)
752
753 patch.set_description(descr)
754 patch.set_authname(author_name)
755 patch.set_authemail(author_email)
756 patch.set_authdate(author_date)
757 patch.set_commname(committer_name)
758 patch.set_commemail(committer_email)
759
760 if unapplied:
761 self.log_patch(patch, 'new')
762
763 patches = [patch.get_name()] + self.get_unapplied()
764
765 f = file(self.__unapplied_file, 'w+')
766 f.writelines([line + '\n' for line in patches])
767 f.close()
768 elif before_existing:
769 self.log_patch(patch, 'new')
770
771 insert_string(self.__applied_file, patch.get_name())
772 if not self.get_current():
773 self.__set_current(name)
774 else:
775 append_string(self.__applied_file, patch.get_name())
776 self.__set_current(name)
777 if refresh:
778 self.refresh_patch(cache_update = False, log = 'new')
779
780 def delete_patch(self, name):
781 """Deletes a patch
782 """
783 patch = Patch(name, self.__patch_dir, self.__refs_dir)
784
785 if self.__patch_is_current(patch):
786 self.pop_patch(name)
787 elif self.patch_applied(name):
788 raise StackException, 'Cannot remove an applied patch, "%s", ' \
789 'which is not current' % name
790 elif not name in self.get_unapplied():
791 raise StackException, 'Unknown patch "%s"' % name
792
793 # save the commit id to a trash file
794 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
795
796 patch.delete()
797
798 unapplied = self.get_unapplied()
799 unapplied.remove(name)
800 f = file(self.__unapplied_file, 'w+')
801 f.writelines([line + '\n' for line in unapplied])
802 f.close()
803
804 if self.patch_hidden(name):
805 self.unhide_patch(name)
806
807 self.__begin_stack_check()
808
809 def forward_patches(self, names):
810 """Try to fast-forward an array of patches.
811
812 On return, patches in names[0:returned_value] have been pushed on the
813 stack. Apply the rest with push_patch
814 """
815 unapplied = self.get_unapplied()
816 self.__begin_stack_check()
817
818 forwarded = 0
819 top = git.get_head()
820
821 for name in names:
822 assert(name in unapplied)
823
824 patch = Patch(name, self.__patch_dir, self.__refs_dir)
825
826 head = top
827 bottom = patch.get_bottom()
828 top = patch.get_top()
829
830 # top != bottom always since we have a commit for each patch
831 if head == bottom:
832 # reset the backup information. No logging since the
833 # patch hasn't changed
834 patch.set_bottom(head, backup = True)
835 patch.set_top(top, backup = True)
836
837 else:
838 head_tree = git.get_commit(head).get_tree()
839 bottom_tree = git.get_commit(bottom).get_tree()
840 if head_tree == bottom_tree:
841 # We must just reparent this patch and create a new commit
842 # for it
843 descr = patch.get_description()
844 author_name = patch.get_authname()
845 author_email = patch.get_authemail()
846 author_date = patch.get_authdate()
847 committer_name = patch.get_commname()
848 committer_email = patch.get_commemail()
849
850 top_tree = git.get_commit(top).get_tree()
851
852 top = git.commit(message = descr, parents = [head],
853 cache_update = False,
854 tree_id = top_tree,
855 allowempty = True,
856 author_name = author_name,
857 author_email = author_email,
858 author_date = author_date,
859 committer_name = committer_name,
860 committer_email = committer_email)
861
862 patch.set_bottom(head, backup = True)
863 patch.set_top(top, backup = True)
864
865 self.log_patch(patch, 'push(f)')
866 else:
867 top = head
868 # stop the fast-forwarding, must do a real merge
869 break
870
871 forwarded+=1
872 unapplied.remove(name)
873
874 if forwarded == 0:
875 return 0
876
877 git.switch(top)
878
879 append_strings(self.__applied_file, names[0:forwarded])
880
881 f = file(self.__unapplied_file, 'w+')
882 f.writelines([line + '\n' for line in unapplied])
883 f.close()
884
885 self.__set_current(name)
886
887 return forwarded
888
889 def merged_patches(self, names):
890 """Test which patches were merged upstream by reverse-applying
891 them in reverse order. The function returns the list of
892 patches detected to have been applied. The state of the tree
893 is restored to the original one
894 """
895 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
896 for name in names]
897 patches.reverse()
898
899 merged = []
900 for p in patches:
901 if git.apply_diff(p.get_top(), p.get_bottom()):
902 merged.append(p.get_name())
903 merged.reverse()
904
905 git.reset()
906
907 return merged
908
909 def push_patch(self, name, empty = False):
910 """Pushes a patch on the stack
911 """
912 unapplied = self.get_unapplied()
913 assert(name in unapplied)
914
915 self.__begin_stack_check()
916
917 patch = Patch(name, self.__patch_dir, self.__refs_dir)
918
919 head = git.get_head()
920 bottom = patch.get_bottom()
921 top = patch.get_top()
922
923 ex = None
924 modified = False
925
926 # top != bottom always since we have a commit for each patch
927 if empty:
928 # just make an empty patch (top = bottom = HEAD). This
929 # option is useful to allow undoing already merged
930 # patches. The top is updated by refresh_patch since we
931 # need an empty commit
932 patch.set_bottom(head, backup = True)
933 patch.set_top(head, backup = True)
934 modified = True
935 elif head == bottom:
936 # reset the backup information. No need for logging
937 patch.set_bottom(bottom, backup = True)
938 patch.set_top(top, backup = True)
939
940 git.switch(top)
941 else:
942 # new patch needs to be refreshed.
943 # The current patch is empty after merge.
944 patch.set_bottom(head, backup = True)
945 patch.set_top(head, backup = True)
946
947 # Try the fast applying first. If this fails, fall back to the
948 # three-way merge
949 if not git.apply_diff(bottom, top):
950 # if git.apply_diff() fails, the patch requires a diff3
951 # merge and can be reported as modified
952 modified = True
953
954 # merge can fail but the patch needs to be pushed
955 try:
956 git.merge(bottom, head, top, recursive = True)
957 except git.GitException, ex:
958 print >> sys.stderr, \
959 'The merge failed during "push". ' \
960 'Use "refresh" after fixing the conflicts'
961
962 append_string(self.__applied_file, name)
963
964 unapplied.remove(name)
965 f = file(self.__unapplied_file, 'w+')
966 f.writelines([line + '\n' for line in unapplied])
967 f.close()
968
969 self.__set_current(name)
970
971 # head == bottom case doesn't need to refresh the patch
972 if empty or head != bottom:
973 if not ex:
974 # if the merge was OK and no conflicts, just refresh the patch
975 # The GIT cache was already updated by the merge operation
976 if modified:
977 log = 'push(m)'
978 else:
979 log = 'push'
980 self.refresh_patch(cache_update = False, log = log)
981 else:
982 # we store the correctly merged files only for
983 # tracking the conflict history. Note that the
984 # git.merge() operations shouls always leave the index
985 # in a valid state (i.e. only stage 0 files)
986 self.refresh_patch(cache_update = False, log = 'push(c)')
987 raise StackException, str(ex)
988
989 return modified
990
991 def undo_push(self):
992 name = self.get_current()
993 assert(name)
994
995 patch = Patch(name, self.__patch_dir, self.__refs_dir)
996 old_bottom = patch.get_old_bottom()
997 old_top = patch.get_old_top()
998
999 # the top of the patch is changed by a push operation only
1000 # together with the bottom (otherwise the top was probably
1001 # modified by 'refresh'). If they are both unchanged, there
1002 # was a fast forward
1003 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1004 raise StackException, 'No undo information available'
1005
1006 git.reset()
1007 self.pop_patch(name)
1008 ret = patch.restore_old_boundaries()
1009 if ret:
1010 self.log_patch(patch, 'undo')
1011
1012 return ret
1013
1014 def pop_patch(self, name, keep = False):
1015 """Pops the top patch from the stack
1016 """
1017 applied = self.get_applied()
1018 applied.reverse()
1019 assert(name in applied)
1020
1021 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1022
1023 # only keep the local changes
1024 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1025 raise StackException, \
1026 'Failed to pop patches while preserving the local changes'
1027
1028 git.switch(patch.get_bottom(), keep)
1029
1030 # save the new applied list
1031 idx = applied.index(name) + 1
1032
1033 popped = applied[:idx]
1034 popped.reverse()
1035 unapplied = popped + self.get_unapplied()
1036
1037 f = file(self.__unapplied_file, 'w+')
1038 f.writelines([line + '\n' for line in unapplied])
1039 f.close()
1040
1041 del applied[:idx]
1042 applied.reverse()
1043
1044 f = file(self.__applied_file, 'w+')
1045 f.writelines([line + '\n' for line in applied])
1046 f.close()
1047
1048 if applied == []:
1049 self.__set_current(None)
1050 else:
1051 self.__set_current(applied[-1])
1052
1053 self.__end_stack_check()
1054
1055 def empty_patch(self, name):
1056 """Returns True if the patch is empty
1057 """
1058 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1059 bottom = patch.get_bottom()
1060 top = patch.get_top()
1061
1062 if bottom == top:
1063 return True
1064 elif git.get_commit(top).get_tree() \
1065 == git.get_commit(bottom).get_tree():
1066 return True
1067
1068 return False
1069
1070 def rename_patch(self, oldname, newname):
1071 applied = self.get_applied()
1072 unapplied = self.get_unapplied()
1073
1074 if oldname == newname:
1075 raise StackException, '"To" name and "from" name are the same'
1076
1077 if newname in applied or newname in unapplied:
1078 raise StackException, 'Patch "%s" already exists' % newname
1079
1080 if self.patch_hidden(oldname):
1081 self.unhide_patch(oldname)
1082 self.hide_patch(newname)
1083
1084 if oldname in unapplied:
1085 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1086 unapplied[unapplied.index(oldname)] = newname
1087
1088 f = file(self.__unapplied_file, 'w+')
1089 f.writelines([line + '\n' for line in unapplied])
1090 f.close()
1091 elif oldname in applied:
1092 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1093 if oldname == self.get_current():
1094 self.__set_current(newname)
1095
1096 applied[applied.index(oldname)] = newname
1097
1098 f = file(self.__applied_file, 'w+')
1099 f.writelines([line + '\n' for line in applied])
1100 f.close()
1101 else:
1102 raise StackException, 'Unknown patch "%s"' % oldname
1103
1104 def log_patch(self, patch, message):
1105 """Generate a log commit for a patch
1106 """
1107 top = git.get_commit(patch.get_top())
1108 msg = '%s\t%s' % (message, top.get_id_hash())
1109
1110 old_log = patch.get_log()
1111 if old_log:
1112 parents = [old_log]
1113 else:
1114 parents = []
1115
1116 log = git.commit(message = msg, parents = parents,
1117 cache_update = False, tree_id = top.get_tree(),
1118 allowempty = True)
1119 patch.set_log(log)
1120
1121 def hide_patch(self, name):
1122 """Add the patch to the hidden list.
1123 """
1124 if not self.patch_exists(name):
1125 raise StackException, 'Unknown patch "%s"' % name
1126 elif self.patch_hidden(name):
1127 raise StackException, 'Patch "%s" already hidden' % name
1128
1129 append_string(self.__hidden_file, name)
1130
1131 def unhide_patch(self, name):
1132 """Add the patch to the hidden list.
1133 """
1134 if not self.patch_exists(name):
1135 raise StackException, 'Unknown patch "%s"' % name
1136 hidden = self.get_hidden()
1137 if not name in hidden:
1138 raise StackException, 'Patch "%s" not hidden' % name
1139
1140 hidden.remove(name)
1141
1142 f = file(self.__hidden_file, 'w+')
1143 f.writelines([line + '\n' for line in hidden])
1144 f.close()