5237084ce04bf00e893599df0aacbb7a9ff0840b
1 """Basic quilt-like functionality
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
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.
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.
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
23 from stgit
.utils
import *
24 from stgit
import git
, basedir
, templates
25 from stgit
.config
import config
28 # stack exception class
29 class StackException(Exception):
34 self
.should_print
= True
35 def __call__(self
, x
, until_test
, prefix
):
37 self
.should_print
= False
39 return x
[0:len(prefix
)] != prefix
45 __comment_prefix
= 'STG:'
46 __patch_prefix
= 'STG_PATCH:'
48 def __clean_comments(f
):
49 """Removes lines marked for status in a commit file
53 # remove status-prefixed lines
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
)]
60 # remove empty lines at the end
61 while len(lines
) != 0 and lines
[-1] == '\n':
64 f
.seek(0); f
.truncate()
67 def edit_file(series
, line
, comment
, show_patch
= True):
68 fname
= '.stgitmsg.txt'
69 tmpl
= templates
.get_template('patchdescr.tmpl')
78 print >> f
, __comment_prefix
, comment
79 print >> f
, __comment_prefix
, \
80 'Lines prefixed with "%s" will be automatically removed.' \
82 print >> f
, __comment_prefix
, \
83 'Trailing empty lines will be automatically removed.'
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
)
90 #Vim modeline must be near the end.
91 print >> f
, __comment_prefix
, 'vi: set textwidth=75 filetype=diff nobackup:'
95 if config
.has_option('stgit', 'editor'):
96 editor
= config
.get('stgit', 'editor')
97 elif 'EDITOR' in os
.environ
:
98 editor
= os
.environ
['EDITOR']
101 editor
+= ' %s' % fname
103 print 'Invoking the editor: "%s"...' % editor
,
105 print 'done (exit code: %d)' % os
.system(editor
)
107 f
= file(fname
, 'r+')
123 """An object with stgit-like properties stored as files in a directory
125 def _set_dir(self
, dir):
130 def create_empty_field(self
, name
):
131 create_empty_file(os
.path
.join(self
.__dir
, name
))
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
)
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
):
152 class Patch(StgitObject
):
153 """Basic patch implementation
155 def __init__(self
, name
, series_dir
, refs_dir
):
156 self
.__series_dir
= series_dir
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')
165 os
.mkdir(self
._dir())
166 self
.create_empty_field('bottom')
167 self
.create_empty_field('top')
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
)
180 def rename(self
, newname
):
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')
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
)
195 def __update_top_ref(self
, ref
):
196 write_string(self
.__top_ref_file
, ref
)
198 def __update_log_ref(self
, ref
):
199 write_string(self
.__log_ref_file
, ref
)
201 def update_top_ref(self
):
204 self
.__update_top_ref(top
)
206 def get_old_bottom(self
):
207 return self
._get_field('bottom.old')
209 def get_bottom(self
):
210 return self
._get_field('bottom')
212 def set_bottom(self
, value
, backup
= False):
214 curr
= self
._get_field('bottom')
215 self
._set_field('bottom.old', curr
)
216 self
._set_field('bottom', value
)
218 def get_old_top(self
):
219 return self
._get_field('top.old')
222 return self
._get_field('top')
224 def set_top(self
, value
, backup
= False):
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
)
231 def restore_old_boundaries(self
):
232 bottom
= self
._get_field('bottom.old')
233 top
= self
._get_field('top.old')
236 self
._set_field('bottom', bottom
)
237 self
._set_field('top', top
)
238 self
.__update_top_ref(top
)
243 def get_description(self
):
244 return self
._get_field('description', True)
246 def set_description(self
, line
):
247 self
._set_field('description', line
, True)
249 def get_authname(self
):
250 return self
._get_field('authname')
252 def set_authname(self
, name
):
253 self
._set_field('authname', name
or git
.author().name
)
255 def get_authemail(self
):
256 return self
._get_field('authemail')
258 def set_authemail(self
, email
):
259 self
._set_field('authemail', email
or git
.author().email
)
261 def get_authdate(self
):
262 return self
._get_field('authdate')
264 def set_authdate(self
, date
):
265 self
._set_field('authdate', date
or git
.author().date
)
267 def get_commname(self
):
268 return self
._get_field('commname')
270 def set_commname(self
, name
):
271 self
._set_field('commname', name
or git
.committer().name
)
273 def get_commemail(self
):
274 return self
._get_field('commemail')
276 def set_commemail(self
, email
):
277 self
._set_field('commemail', email
or git
.committer().email
)
280 return self
._get_field('log')
282 def set_log(self
, value
, backup
= False):
283 self
._set_field('log', value
)
284 self
.__update_log_ref(value
)
287 class Series(StgitObject
):
288 """Class including the operations on series
290 def __init__(self
, name
= None):
291 """Takes a series name as the parameter.
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
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',
305 self
.__base_file
= os
.path
.join(self
.__base_dir
, 'refs', 'bases',
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')
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()
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()
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
)
330 def get_branch(self
):
331 """Return the branch name for the Series object
335 def __set_current(self
, name
):
336 """Sets the topmost patch
338 self
._set_field('current', name
)
340 def get_patch(self
, name
):
341 """Return a Patch object for the given name
343 return Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
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()
351 return Patch(crt
, self
.__patch_dir
, self
.__refs_dir
)
353 def get_current(self
):
354 """Return the name of the topmost patch, or None if there is
356 name
= self
._get_field('current')
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()]
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()]
378 def get_hidden(self
):
379 if not os
.path
.isfile(self
.__hidden_file
):
381 f
= file(self
.__hidden_file
)
382 names
= [line
.strip() for line
in f
.readlines()]
386 def get_base_file(self
):
387 self
.__begin_stack_check()
388 return self
.__base_file
390 def get_protected(self
):
391 return os
.path
.isfile(os
.path
.join(self
._dir(), 'protected'))
394 protect_file
= os
.path
.join(self
._dir(), 'protected')
395 if not os
.path
.isfile(protect_file
):
396 create_empty_file(protect_file
)
399 protect_file
= os
.path
.join(self
._dir(), 'protected')
400 if os
.path
.isfile(protect_file
):
401 os
.remove(protect_file
)
403 def get_description(self
):
404 return self
._get_field('description') or ''
406 def set_description(self
, line
):
407 self
._set_field('description', line
)
409 def __patch_is_current(self
, patch
):
410 return patch
.get_name() == self
.get_current()
412 def patch_applied(self
, name
):
413 """Return true if the patch exists in the applied list
415 return name
in self
.get_applied()
417 def patch_unapplied(self
, name
):
418 """Return true if the patch exists in the unapplied list
420 return name
in self
.get_unapplied()
422 def patch_hidden(self
, name
):
423 """Return true if the patch is hidden.
425 return name
in self
.get_hidden()
427 def patch_exists(self
, name
):
428 """Return true if there is a patch with the given name, false
430 return self
.patch_applied(name
) or self
.patch_unapplied(name
)
432 def __begin_stack_check(self
):
433 """Save the current HEAD into .git/refs/heads/base if the stack
436 if len(self
.get_applied()) == 0:
437 head
= git
.get_head()
438 write_string(self
.__base_file
, head
)
440 def __end_stack_check(self
):
441 """Remove .git/refs/heads/base if the stack is empty.
442 This warning should never happen
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'
448 def head_top_equal(self
):
449 """Return true if the head and the top are the same
451 crt
= self
.get_current_patch()
453 # we don't care, no patches applied
455 return git
.get_head() == crt
.get_top()
457 def is_initialised(self
):
458 """Checks if series is already initialised
460 return os
.path
.isdir(self
.__patch_dir
)
462 def init(self
, create_at
=False):
463 """Initialises the stgit series
465 bases_dir
= os
.path
.join(self
.__base_dir
, 'refs', 'bases')
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'
474 if (create_at
!=False):
475 git
.create_branch(self
.__name
, create_at
)
477 os
.makedirs(self
.__patch_dir
)
479 create_dirs(bases_dir
)
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()
489 """Either convert to use a separate patch directory, or
490 unconvert to place the patches in the same directory with
493 if self
.__patch_dir
== self
._dir():
494 print 'Converting old-style to new-style...',
497 self
.__patch_dir
= os
.path
.join(self
._dir(), 'patches')
498 os
.makedirs(self
.__patch_dir
)
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
)
508 print 'Converting new-style to old-style...',
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
)
516 if not os
.listdir(self
.__patch_dir
):
517 os
.rmdir(self
.__patch_dir
)
520 print 'Patch directory %s is not empty.' % self
.__name
522 self
.__patch_dir
= self
._dir()
524 def rename(self
, to_name
):
527 to_stack
= Series(to_name
)
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
)
534 git
.rename_branch(self
.__name
, to_name
)
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
)
546 self
.__init__(to_name
)
548 def clone(self
, target_series
):
552 # allow cloning of branches not under StGIT control
553 base
= read_string(self
.get_base_file())
555 base
= git
.get_head()
556 Series(target_series
).init(create_at
= base
)
557 new_series
= Series(target_series
)
559 # generate an artificial description file
560 new_series
.set_description('clone of "%s"' % self
.__name
)
562 # clone self's entire series as unapplied patches
564 # allow cloning of branches not under StGIT control
565 applied
= self
.get_applied()
566 unapplied
= self
.get_unapplied()
567 patches
= applied
+ unapplied
570 patches
= applied
= unapplied
= []
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())
581 # fast forward the cloned series to self's top
582 new_series
.forward_patches(applied
)
584 def delete(self
, force
= False):
585 """Deletes an stgit series
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'
593 Patch(p
, self
.__patch_dir
, self
.__refs_dir
).delete()
595 # remove the trash directory
596 for fname
in os
.listdir(self
.__trash_dir
):
598 os
.rmdir(self
.__trash_dir
)
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
)
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'),
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'),
625 print 'Refs directory %s is not empty.' % self
.__refs_dir
627 if os
.path
.exists(self
.__base_file
):
628 remove_file_and_dirs(
629 os
.path
.join(self
.__base_dir
, 'refs', 'bases'), self
.__name
)
631 def refresh_patch(self
, files
= None, message
= None, edit
= False,
634 author_name
= None, author_email
= 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
640 name
= self
.get_current()
642 raise StackException
, 'No patches applied'
644 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
646 descr
= patch
.get_description()
647 if not (message
or descr
):
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
)
659 author_name
= patch
.get_authname()
661 author_email
= patch
.get_authemail()
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()
670 descr
= '%s\n%s: %s <%s>\n' %
(descr
.rstrip(), sign_str
,
671 committer_name
, committer_email
)
673 bottom
= patch
.get_bottom()
675 commit_id
= git
.commit(files
= files
,
676 message
= descr
, parents
= [bottom
],
677 cache_update
= cache_update
,
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
)
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
)
695 self
.log_patch(patch
, log
)
699 def undo_refresh(self
):
700 """Undo the patch boundaries changes caused by 'refresh'
702 name
= self
.get_current()
705 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
706 old_bottom
= patch
.get_old_bottom()
707 old_top
= patch
.get_old_top()
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'
715 git
.reset(tree_id
= old_top
, check_out
= False)
716 if patch
.restore_old_boundaries():
717 self
.log_patch(patch
, 'undo')
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
727 if self
.patch_applied(name
) or self
.patch_unapplied(name
):
728 raise StackException
, 'Patch "%s" already exists' % name
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
)
737 head
= git
.get_head()
739 self
.__begin_stack_check()
741 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
745 patch
.set_bottom(bottom
)
747 patch
.set_bottom(head
)
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
)
761 self
.log_patch(patch
, 'new')
763 patches
= [patch
.get_name()] + self
.get_unapplied()
765 f
= file(self
.__unapplied_file
, 'w+')
766 f
.writelines([line
+ '\n' for line
in patches
])
768 elif before_existing
:
769 self
.log_patch(patch
, 'new')
771 insert_string(self
.__applied_file
, patch
.get_name())
772 if not self
.get_current():
773 self
.__set_current(name
)
775 append_string(self
.__applied_file
, patch
.get_name())
776 self
.__set_current(name
)
778 self
.refresh_patch(cache_update
= False, log
= 'new')
780 def delete_patch(self
, name
):
783 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
785 if self
.__patch_is_current(patch
):
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
793 # save the commit id to a trash file
794 write_string(os
.path
.join(self
.__trash_dir
, name
), patch
.get_top())
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
])
804 if self
.patch_hidden(name
):
805 self
.unhide_patch(name
)
807 self
.__begin_stack_check()
809 def forward_patches(self
, names
):
810 """Try to fast-forward an array of patches.
812 On return, patches in names[0:returned_value] have been pushed on the
813 stack. Apply the rest with push_patch
815 unapplied
= self
.get_unapplied()
816 self
.__begin_stack_check()
822 assert(name
in unapplied
)
824 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
827 bottom
= patch
.get_bottom()
828 top
= patch
.get_top()
830 # top != bottom always since we have a commit for each patch
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)
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
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()
850 top_tree
= git
.get_commit(top
).get_tree()
852 top
= git
.commit(message
= descr
, parents
= [head
],
853 cache_update
= False,
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
)
862 patch
.set_bottom(head
, backup
= True)
863 patch
.set_top(top
, backup
= True)
865 self
.log_patch(patch
, 'push(f)')
868 # stop the fast-forwarding, must do a real merge
872 unapplied
.remove(name
)
879 append_strings(self
.__applied_file
, names
[0:forwarded
])
881 f
= file(self
.__unapplied_file
, 'w+')
882 f
.writelines([line
+ '\n' for line
in unapplied
])
885 self
.__set_current(name
)
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
895 patches
= [Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
901 if git
.apply_diff(p
.get_top(), p
.get_bottom()):
902 merged
.append(p
.get_name())
909 def push_patch(self
, name
, empty
= False):
910 """Pushes a patch on the stack
912 unapplied
= self
.get_unapplied()
913 assert(name
in unapplied
)
915 self
.__begin_stack_check()
917 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
919 head
= git
.get_head()
920 bottom
= patch
.get_bottom()
921 top
= patch
.get_top()
926 # top != bottom always since we have a commit for each patch
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)
936 # reset the backup information. No need for logging
937 patch
.set_bottom(bottom
, backup
= True)
938 patch
.set_top(top
, backup
= True)
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)
947 # Try the fast applying first. If this fails, fall back to the
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
954 # merge can fail but the patch needs to be pushed
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'
962 append_string(self
.__applied_file
, name
)
964 unapplied
.remove(name
)
965 f
= file(self
.__unapplied_file
, 'w+')
966 f
.writelines([line
+ '\n' for line
in unapplied
])
969 self
.__set_current(name
)
971 # head == bottom case doesn't need to refresh the patch
972 if empty
or head
!= bottom
:
974 # if the merge was OK and no conflicts, just refresh the patch
975 # The GIT cache was already updated by the merge operation
980 self
.refresh_patch(cache_update
= False, log
= log
)
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
)
992 name
= self
.get_current()
995 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
996 old_bottom
= patch
.get_old_bottom()
997 old_top
= patch
.get_old_top()
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'
1007 self
.pop_patch(name
)
1008 ret
= patch
.restore_old_boundaries()
1010 self
.log_patch(patch
, 'undo')
1014 def pop_patch(self
, name
, keep
= False):
1015 """Pops the top patch from the stack
1017 applied
= self
.get_applied()
1019 assert(name
in applied
)
1021 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
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'
1028 git
.switch(patch
.get_bottom(), keep
)
1030 # save the new applied list
1031 idx
= applied
.index(name
) + 1
1033 popped
= applied
[:idx
]
1035 unapplied
= popped
+ self
.get_unapplied()
1037 f
= file(self
.__unapplied_file
, 'w+')
1038 f
.writelines([line
+ '\n' for line
in unapplied
])
1044 f
= file(self
.__applied_file
, 'w+')
1045 f
.writelines([line
+ '\n' for line
in applied
])
1049 self
.__set_current(None)
1051 self
.__set_current(applied
[-1])
1053 self
.__end_stack_check()
1055 def empty_patch(self
, name
):
1056 """Returns True if the patch is empty
1058 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
1059 bottom
= patch
.get_bottom()
1060 top
= patch
.get_top()
1064 elif git
.get_commit(top
).get_tree() \
1065 == git
.get_commit(bottom
).get_tree():
1070 def rename_patch(self
, oldname
, newname
):
1071 applied
= self
.get_applied()
1072 unapplied
= self
.get_unapplied()
1074 if oldname
== newname
:
1075 raise StackException
, '"To" name and "from" name are the same'
1077 if newname
in applied
or newname
in unapplied
:
1078 raise StackException
, 'Patch "%s" already exists' % newname
1080 if self
.patch_hidden(oldname
):
1081 self
.unhide_patch(oldname
)
1082 self
.hide_patch(newname
)
1084 if oldname
in unapplied
:
1085 Patch(oldname
, self
.__patch_dir
, self
.__refs_dir
).rename(newname
)
1086 unapplied
[unapplied
.index(oldname
)] = newname
1088 f
= file(self
.__unapplied_file
, 'w+')
1089 f
.writelines([line
+ '\n' for line
in unapplied
])
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
)
1096 applied
[applied
.index(oldname
)] = newname
1098 f
= file(self
.__applied_file
, 'w+')
1099 f
.writelines([line
+ '\n' for line
in applied
])
1102 raise StackException
, 'Unknown patch "%s"' % oldname
1104 def log_patch(self
, patch
, message
):
1105 """Generate a log commit for a patch
1107 top
= git
.get_commit(patch
.get_top())
1108 msg
= '%s\t%s' %
(message
, top
.get_id_hash())
1110 old_log
= patch
.get_log()
1116 log
= git
.commit(message
= msg
, parents
= parents
,
1117 cache_update
= False, tree_id
= top
.get_tree(),
1121 def hide_patch(self
, name
):
1122 """Add the patch to the hidden list.
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
1129 append_string(self
.__hidden_file
, name
)
1131 def unhide_patch(self
, name
):
1132 """Add the patch to the hidden list.
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
1142 f
= file(self
.__hidden_file
, 'w+')
1143 f
.writelines([line
+ '\n' for line
in hidden
])