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