6f0f67a568230a581181517723ca946d6ef47169
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 editor
= config
.get('stgit.editor')
98 elif 'EDITOR' in os
.environ
:
99 editor
= os
.environ
['EDITOR']
102 editor
+= ' %s' % fname
104 print 'Invoking the editor: "%s"...' % editor
,
106 print 'done (exit code: %d)' % os
.system(editor
)
108 f
= file(fname
, 'r+')
124 """An object with stgit-like properties stored as files in a directory
126 def _set_dir(self
, dir):
131 def create_empty_field(self
, name
):
132 create_empty_file(os
.path
.join(self
.__dir
, name
))
134 def _get_field(self
, name
, multiline
= False):
135 id_file
= os
.path
.join(self
.__dir
, name
)
136 if os
.path
.isfile(id_file
):
137 line
= read_string(id_file
, multiline
)
145 def _set_field(self
, name
, value
, multiline
= False):
146 fname
= os
.path
.join(self
.__dir
, name
)
147 if value
and value
!= '':
148 write_string(fname
, value
, multiline
)
149 elif os
.path
.isfile(fname
):
153 class Patch(StgitObject
):
154 """Basic patch implementation
156 def __init__(self
, name
, series_dir
, refs_dir
):
157 self
.__series_dir
= series_dir
159 self
._set_dir(os
.path
.join(self
.__series_dir
, self
.__name
))
160 self
.__refs_dir
= refs_dir
161 self
.__top_ref_file
= os
.path
.join(self
.__refs_dir
, self
.__name
)
162 self
.__log_ref_file
= os
.path
.join(self
.__refs_dir
,
163 self
.__name
+ '.log')
166 os
.mkdir(self
._dir())
167 self
.create_empty_field('bottom')
168 self
.create_empty_field('top')
171 for f
in os
.listdir(self
._dir()):
172 os
.remove(os
.path
.join(self
._dir(), f
))
173 os
.rmdir(self
._dir())
174 os
.remove(self
.__top_ref_file
)
175 if os
.path
.exists(self
.__log_ref_file
):
176 os
.remove(self
.__log_ref_file
)
181 def rename(self
, newname
):
183 old_top_ref_file
= self
.__top_ref_file
184 old_log_ref_file
= self
.__log_ref_file
185 self
.__name
= newname
186 self
._set_dir(os
.path
.join(self
.__series_dir
, self
.__name
))
187 self
.__top_ref_file
= os
.path
.join(self
.__refs_dir
, self
.__name
)
188 self
.__log_ref_file
= os
.path
.join(self
.__refs_dir
,
189 self
.__name
+ '.log')
191 os
.rename(olddir
, self
._dir())
192 os
.rename(old_top_ref_file
, self
.__top_ref_file
)
193 if os
.path
.exists(old_log_ref_file
):
194 os
.rename(old_log_ref_file
, self
.__log_ref_file
)
196 def __update_top_ref(self
, ref
):
197 write_string(self
.__top_ref_file
, ref
)
199 def __update_log_ref(self
, ref
):
200 write_string(self
.__log_ref_file
, ref
)
202 def update_top_ref(self
):
205 self
.__update_top_ref(top
)
207 def get_old_bottom(self
):
208 return self
._get_field('bottom.old')
210 def get_bottom(self
):
211 return self
._get_field('bottom')
213 def set_bottom(self
, value
, backup
= False):
215 curr
= self
._get_field('bottom')
216 self
._set_field('bottom.old', curr
)
217 self
._set_field('bottom', value
)
219 def get_old_top(self
):
220 return self
._get_field('top.old')
223 return self
._get_field('top')
225 def set_top(self
, value
, backup
= False):
227 curr
= self
._get_field('top')
228 self
._set_field('top.old', curr
)
229 self
._set_field('top', value
)
230 self
.__update_top_ref(value
)
232 def restore_old_boundaries(self
):
233 bottom
= self
._get_field('bottom.old')
234 top
= self
._get_field('top.old')
237 self
._set_field('bottom', bottom
)
238 self
._set_field('top', top
)
239 self
.__update_top_ref(top
)
244 def get_description(self
):
245 return self
._get_field('description', True)
247 def set_description(self
, line
):
248 self
._set_field('description', line
, True)
250 def get_authname(self
):
251 return self
._get_field('authname')
253 def set_authname(self
, name
):
254 self
._set_field('authname', name
or git
.author().name
)
256 def get_authemail(self
):
257 return self
._get_field('authemail')
259 def set_authemail(self
, email
):
260 self
._set_field('authemail', email
or git
.author().email
)
262 def get_authdate(self
):
263 return self
._get_field('authdate')
265 def set_authdate(self
, date
):
266 self
._set_field('authdate', date
or git
.author().date
)
268 def get_commname(self
):
269 return self
._get_field('commname')
271 def set_commname(self
, name
):
272 self
._set_field('commname', name
or git
.committer().name
)
274 def get_commemail(self
):
275 return self
._get_field('commemail')
277 def set_commemail(self
, email
):
278 self
._set_field('commemail', email
or git
.committer().email
)
281 return self
._get_field('log')
283 def set_log(self
, value
, backup
= False):
284 self
._set_field('log', value
)
285 self
.__update_log_ref(value
)
288 class Series(StgitObject
):
289 """Class including the operations on series
291 def __init__(self
, name
= None):
292 """Takes a series name as the parameter.
298 self
.__name
= git
.get_head_file()
299 self
.__base_dir
= basedir
.get()
300 except git
.GitException
, ex
:
301 raise StackException
, 'GIT tree not initialised: %s' % ex
303 self
._set_dir(os
.path
.join(self
.__base_dir
, 'patches', self
.__name
))
304 self
.__refs_dir
= os
.path
.join(self
.__base_dir
, 'refs', 'patches',
306 self
.__base_file
= os
.path
.join(self
.__base_dir
, 'refs', 'bases',
309 self
.__applied_file
= os
.path
.join(self
._dir(), 'applied')
310 self
.__unapplied_file
= os
.path
.join(self
._dir(), 'unapplied')
311 self
.__hidden_file
= os
.path
.join(self
._dir(), 'hidden')
312 self
.__current_file
= os
.path
.join(self
._dir(), 'current')
313 self
.__descr_file
= os
.path
.join(self
._dir(), 'description')
315 # where this series keeps its patches
316 self
.__patch_dir
= os
.path
.join(self
._dir(), 'patches')
317 if not os
.path
.isdir(self
.__patch_dir
):
318 self
.__patch_dir
= self
._dir()
320 # if no __refs_dir, create and populate it (upgrade old repositories)
321 if self
.is_initialised() and not os
.path
.isdir(self
.__refs_dir
):
322 os
.makedirs(self
.__refs_dir
)
323 for patch
in self
.get_applied() + self
.get_unapplied():
324 self
.get_patch(patch
).update_top_ref()
327 self
.__trash_dir
= os
.path
.join(self
._dir(), 'trash')
328 if self
.is_initialised() and not os
.path
.isdir(self
.__trash_dir
):
329 os
.makedirs(self
.__trash_dir
)
331 def __patch_name_valid(self
, name
):
332 """Raise an exception if the patch name is not valid.
334 if not name
or re
.search('[^\w.-]', name
):
335 raise StackException
, 'Invalid patch name: "%s"' % name
337 def get_branch(self
):
338 """Return the branch name for the Series object
342 def __set_current(self
, name
):
343 """Sets the topmost patch
345 self
._set_field('current', name
)
347 def get_patch(self
, name
):
348 """Return a Patch object for the given name
350 return Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
352 def get_current_patch(self
):
353 """Return a Patch object representing the topmost patch, or
354 None if there is no such patch."""
355 crt
= self
.get_current()
358 return Patch(crt
, self
.__patch_dir
, self
.__refs_dir
)
360 def get_current(self
):
361 """Return the name of the topmost patch, or None if there is
363 name
= self
._get_field('current')
369 def get_applied(self
):
370 if not os
.path
.isfile(self
.__applied_file
):
371 raise StackException
, 'Branch "%s" not initialised' % self
.__name
372 f
= file(self
.__applied_file
)
373 names
= [line
.strip() for line
in f
.readlines()]
377 def get_unapplied(self
):
378 if not os
.path
.isfile(self
.__unapplied_file
):
379 raise StackException
, 'Branch "%s" not initialised' % self
.__name
380 f
= file(self
.__unapplied_file
)
381 names
= [line
.strip() for line
in f
.readlines()]
385 def get_hidden(self
):
386 if not os
.path
.isfile(self
.__hidden_file
):
388 f
= file(self
.__hidden_file
)
389 names
= [line
.strip() for line
in f
.readlines()]
393 def get_base_file(self
):
394 self
.__begin_stack_check()
395 return self
.__base_file
397 def get_protected(self
):
398 return os
.path
.isfile(os
.path
.join(self
._dir(), 'protected'))
401 protect_file
= os
.path
.join(self
._dir(), 'protected')
402 if not os
.path
.isfile(protect_file
):
403 create_empty_file(protect_file
)
406 protect_file
= os
.path
.join(self
._dir(), 'protected')
407 if os
.path
.isfile(protect_file
):
408 os
.remove(protect_file
)
410 def get_description(self
):
411 return self
._get_field('description') or ''
413 def set_description(self
, line
):
414 self
._set_field('description', line
)
416 def get_parent_remote(self
):
417 value
= config
.get('branch.%s.remote' % self
.__name
)
420 elif 'origin' in git
.remotes_list():
421 print 'Notice: no parent remote declared for stack "%s", defaulting to "origin".' \
422 'Consider setting "branch.%s.remote" with "git repo-config".' \
423 %
(self
.__name
, self
.__name
)
426 raise StackException
, 'Cannot find a parent remote for "%s"' % self
.__name
428 def __set_parent_remote(self
, remote
):
429 value
= config
.set('branch.%s.remote' % self
.__name
, remote
)
431 def get_parent_branch(self
):
432 value
= config
.get('branch.%s.merge' % self
.__name
)
435 elif git
.rev_parse('heads/origin'):
436 print 'Notice: no parent branch declared for stack "%s", defaulting to "heads/origin".' \
437 'Consider setting "branch.%s.merge" with "git repo-config".' \
438 %
(self
.__name
, self
.__name
)
439 return 'heads/origin'
441 raise StackException
, 'Cannot find a parent branch for "%s"' % self
.__name
443 def __set_parent_branch(self
, name
):
444 config
.set('branch.%s.merge' % self
.__name
, name
)
446 def set_parent(self
, remote
, localbranch
):
448 self
.__set_parent_branch(localbranch
)
450 self
.__set_parent_remote(remote
)
452 raise StackException
, 'Remote "%s" without a branch cannot be used as parent' % remote
454 def __patch_is_current(self
, patch
):
455 return patch
.get_name() == self
.get_current()
457 def patch_applied(self
, name
):
458 """Return true if the patch exists in the applied list
460 return name
in self
.get_applied()
462 def patch_unapplied(self
, name
):
463 """Return true if the patch exists in the unapplied list
465 return name
in self
.get_unapplied()
467 def patch_hidden(self
, name
):
468 """Return true if the patch is hidden.
470 return name
in self
.get_hidden()
472 def patch_exists(self
, name
):
473 """Return true if there is a patch with the given name, false
475 return self
.patch_applied(name
) or self
.patch_unapplied(name
)
477 def __begin_stack_check(self
):
478 """Save the current HEAD into .git/refs/heads/base if the stack
481 if len(self
.get_applied()) == 0:
482 head
= git
.get_head()
483 write_string(self
.__base_file
, head
)
485 def __end_stack_check(self
):
486 """Remove .git/refs/heads/base if the stack is empty.
487 This warning should never happen
489 if len(self
.get_applied()) == 0 \
490 and read_string(self
.__base_file
) != git
.get_head():
491 print 'Warning: stack empty but the HEAD and base are different'
493 def head_top_equal(self
):
494 """Return true if the head and the top are the same
496 crt
= self
.get_current_patch()
498 # we don't care, no patches applied
500 return git
.get_head() == crt
.get_top()
502 def is_initialised(self
):
503 """Checks if series is already initialised
505 return os
.path
.isdir(self
.__patch_dir
)
507 def init(self
, create_at
=False, parent_remote
=None, parent_branch
=None):
508 """Initialises the stgit series
510 bases_dir
= os
.path
.join(self
.__base_dir
, 'refs', 'bases')
512 if os
.path
.exists(self
.__patch_dir
):
513 raise StackException
, self
.__patch_dir
+ ' already exists'
514 if os
.path
.exists(self
.__refs_dir
):
515 raise StackException
, self
.__refs_dir
+ ' already exists'
516 if os
.path
.exists(self
.__base_file
):
517 raise StackException
, self
.__base_file
+ ' already exists'
519 if (create_at
!=False):
520 git
.create_branch(self
.__name
, create_at
)
522 os
.makedirs(self
.__patch_dir
)
524 self
.set_parent(parent_remote
, parent_branch
)
526 create_dirs(bases_dir
)
528 self
.create_empty_field('applied')
529 self
.create_empty_field('unapplied')
530 self
.create_empty_field('description')
531 os
.makedirs(os
.path
.join(self
._dir(), 'patches'))
532 os
.makedirs(self
.__refs_dir
)
533 self
.__begin_stack_check()
536 """Either convert to use a separate patch directory, or
537 unconvert to place the patches in the same directory with
540 if self
.__patch_dir
== self
._dir():
541 print 'Converting old-style to new-style...',
544 self
.__patch_dir
= os
.path
.join(self
._dir(), 'patches')
545 os
.makedirs(self
.__patch_dir
)
547 for p
in self
.get_applied() + self
.get_unapplied():
548 src
= os
.path
.join(self
._dir(), p
)
549 dest
= os
.path
.join(self
.__patch_dir
, p
)
555 print 'Converting new-style to old-style...',
558 for p
in self
.get_applied() + self
.get_unapplied():
559 src
= os
.path
.join(self
.__patch_dir
, p
)
560 dest
= os
.path
.join(self
._dir(), p
)
563 if not os
.listdir(self
.__patch_dir
):
564 os
.rmdir(self
.__patch_dir
)
567 print 'Patch directory %s is not empty.' % self
.__name
569 self
.__patch_dir
= self
._dir()
571 def rename(self
, to_name
):
574 to_stack
= Series(to_name
)
576 if to_stack
.is_initialised():
577 raise StackException
, '"%s" already exists' % to_stack
.get_branch()
578 if os
.path
.exists(to_stack
.__base_file
):
579 os
.remove(to_stack
.__base_file
)
581 git
.rename_branch(self
.__name
, to_name
)
583 if os
.path
.isdir(self
._dir()):
584 rename(os
.path
.join(self
.__base_dir
, 'patches'),
585 self
.__name
, to_stack
.__name
)
586 if os
.path
.exists(self
.__base_file
):
587 rename(os
.path
.join(self
.__base_dir
, 'refs', 'bases'),
588 self
.__name
, to_stack
.__name
)
589 if os
.path
.exists(self
.__refs_dir
):
590 rename(os
.path
.join(self
.__base_dir
, 'refs', 'patches'),
591 self
.__name
, to_stack
.__name
)
593 self
.__init__(to_name
)
595 def clone(self
, target_series
):
599 # allow cloning of branches not under StGIT control
600 base
= read_string(self
.get_base_file())
602 base
= git
.get_head()
603 Series(target_series
).init(create_at
= base
)
604 new_series
= Series(target_series
)
606 # generate an artificial description file
607 new_series
.set_description('clone of "%s"' % self
.__name
)
609 # clone self's entire series as unapplied patches
611 # allow cloning of branches not under StGIT control
612 applied
= self
.get_applied()
613 unapplied
= self
.get_unapplied()
614 patches
= applied
+ unapplied
617 patches
= applied
= unapplied
= []
619 patch
= self
.get_patch(p
)
620 new_series
.new_patch(p
, message
= patch
.get_description(),
621 can_edit
= False, unapplied
= True,
622 bottom
= patch
.get_bottom(),
623 top
= patch
.get_top(),
624 author_name
= patch
.get_authname(),
625 author_email
= patch
.get_authemail(),
626 author_date
= patch
.get_authdate())
628 # fast forward the cloned series to self's top
629 new_series
.forward_patches(applied
)
631 def delete(self
, force
= False):
632 """Deletes an stgit series
634 if self
.is_initialised():
635 patches
= self
.get_unapplied() + self
.get_applied()
636 if not force
and patches
:
637 raise StackException
, \
638 'Cannot delete: the series still contains patches'
640 Patch(p
, self
.__patch_dir
, self
.__refs_dir
).delete()
642 # remove the trash directory
643 for fname
in os
.listdir(self
.__trash_dir
):
645 os
.rmdir(self
.__trash_dir
)
647 # FIXME: find a way to get rid of those manual removals
648 # (move functionnality to StgitObject ?)
649 if os
.path
.exists(self
.__applied_file
):
650 os
.remove(self
.__applied_file
)
651 if os
.path
.exists(self
.__unapplied_file
):
652 os
.remove(self
.__unapplied_file
)
653 if os
.path
.exists(self
.__hidden_file
):
654 os
.remove(self
.__hidden_file
)
655 if os
.path
.exists(self
.__current_file
):
656 os
.remove(self
.__current_file
)
657 if os
.path
.exists(self
.__descr_file
):
658 os
.remove(self
.__descr_file
)
659 if not os
.listdir(self
.__patch_dir
):
660 os
.rmdir(self
.__patch_dir
)
662 print 'Patch directory %s is not empty.' % self
.__name
663 if not os
.listdir(self
._dir()):
664 remove_dirs(os
.path
.join(self
.__base_dir
, 'patches'),
667 print 'Series directory %s is not empty.' % self
.__name
668 if not os
.listdir(self
.__refs_dir
):
669 remove_dirs(os
.path
.join(self
.__base_dir
, 'refs', 'patches'),
672 print 'Refs directory %s is not empty.' % self
.__refs_dir
674 if os
.path
.exists(self
.__base_file
):
675 remove_file_and_dirs(
676 os
.path
.join(self
.__base_dir
, 'refs', 'bases'), self
.__name
)
678 def refresh_patch(self
, files
= None, message
= None, edit
= False,
681 author_name
= None, author_email
= None,
683 committer_name
= None, committer_email
= None,
684 backup
= False, sign_str
= None, log
= 'refresh'):
685 """Generates a new commit for the given patch
687 name
= self
.get_current()
689 raise StackException
, 'No patches applied'
691 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
693 descr
= patch
.get_description()
694 if not (message
or descr
):
700 if not message
and edit
:
701 descr
= edit_file(self
, descr
.rstrip(), \
702 'Please edit the description for patch "%s" ' \
703 'above.' % name
, show_patch
)
706 author_name
= patch
.get_authname()
708 author_email
= patch
.get_authemail()
710 author_date
= patch
.get_authdate()
711 if not committer_name
:
712 committer_name
= patch
.get_commname()
713 if not committer_email
:
714 committer_email
= patch
.get_commemail()
717 descr
= '%s\n%s: %s <%s>\n' %
(descr
.rstrip(), sign_str
,
718 committer_name
, committer_email
)
720 bottom
= patch
.get_bottom()
722 commit_id
= git
.commit(files
= files
,
723 message
= descr
, parents
= [bottom
],
724 cache_update
= cache_update
,
726 author_name
= author_name
,
727 author_email
= author_email
,
728 author_date
= author_date
,
729 committer_name
= committer_name
,
730 committer_email
= committer_email
)
732 patch
.set_bottom(bottom
, backup
= backup
)
733 patch
.set_top(commit_id
, backup
= backup
)
734 patch
.set_description(descr
)
735 patch
.set_authname(author_name
)
736 patch
.set_authemail(author_email
)
737 patch
.set_authdate(author_date
)
738 patch
.set_commname(committer_name
)
739 patch
.set_commemail(committer_email
)
742 self
.log_patch(patch
, log
)
746 def undo_refresh(self
):
747 """Undo the patch boundaries changes caused by 'refresh'
749 name
= self
.get_current()
752 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
753 old_bottom
= patch
.get_old_bottom()
754 old_top
= patch
.get_old_top()
756 # the bottom of the patch is not changed by refresh. If the
757 # old_bottom is different, there wasn't any previous 'refresh'
758 # command (probably only a 'push')
759 if old_bottom
!= patch
.get_bottom() or old_top
== patch
.get_top():
760 raise StackException
, 'No undo information available'
762 git
.reset(tree_id
= old_top
, check_out
= False)
763 if patch
.restore_old_boundaries():
764 self
.log_patch(patch
, 'undo')
766 def new_patch(self
, name
, message
= None, can_edit
= True,
767 unapplied
= False, show_patch
= False,
768 top
= None, bottom
= None,
769 author_name
= None, author_email
= None, author_date
= None,
770 committer_name
= None, committer_email
= None,
771 before_existing
= False, refresh
= True):
772 """Creates a new patch
774 self
.__patch_name_valid(name
)
776 if self
.patch_applied(name
) or self
.patch_unapplied(name
):
777 raise StackException
, 'Patch "%s" already exists' % name
779 if not message
and can_edit
:
780 descr
= edit_file(self
, None, \
781 'Please enter the description for patch "%s" ' \
782 'above.' % name
, show_patch
)
786 head
= git
.get_head()
788 self
.__begin_stack_check()
790 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
794 patch
.set_bottom(bottom
)
796 patch
.set_bottom(head
)
802 patch
.set_description(descr
)
803 patch
.set_authname(author_name
)
804 patch
.set_authemail(author_email
)
805 patch
.set_authdate(author_date
)
806 patch
.set_commname(committer_name
)
807 patch
.set_commemail(committer_email
)
810 self
.log_patch(patch
, 'new')
812 patches
= [patch
.get_name()] + self
.get_unapplied()
814 f
= file(self
.__unapplied_file
, 'w+')
815 f
.writelines([line
+ '\n' for line
in patches
])
817 elif before_existing
:
818 self
.log_patch(patch
, 'new')
820 insert_string(self
.__applied_file
, patch
.get_name())
821 if not self
.get_current():
822 self
.__set_current(name
)
824 append_string(self
.__applied_file
, patch
.get_name())
825 self
.__set_current(name
)
827 self
.refresh_patch(cache_update
= False, log
= 'new')
829 def delete_patch(self
, name
):
832 self
.__patch_name_valid(name
)
833 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
835 if self
.__patch_is_current(patch
):
837 elif self
.patch_applied(name
):
838 raise StackException
, 'Cannot remove an applied patch, "%s", ' \
839 'which is not current' % name
840 elif not name
in self
.get_unapplied():
841 raise StackException
, 'Unknown patch "%s"' % name
843 # save the commit id to a trash file
844 write_string(os
.path
.join(self
.__trash_dir
, name
), patch
.get_top())
848 unapplied
= self
.get_unapplied()
849 unapplied
.remove(name
)
850 f
= file(self
.__unapplied_file
, 'w+')
851 f
.writelines([line
+ '\n' for line
in unapplied
])
854 if self
.patch_hidden(name
):
855 self
.unhide_patch(name
)
857 self
.__begin_stack_check()
859 def forward_patches(self
, names
):
860 """Try to fast-forward an array of patches.
862 On return, patches in names[0:returned_value] have been pushed on the
863 stack. Apply the rest with push_patch
865 unapplied
= self
.get_unapplied()
866 self
.__begin_stack_check()
872 assert(name
in unapplied
)
874 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
877 bottom
= patch
.get_bottom()
878 top
= patch
.get_top()
880 # top != bottom always since we have a commit for each patch
882 # reset the backup information. No logging since the
883 # patch hasn't changed
884 patch
.set_bottom(head
, backup
= True)
885 patch
.set_top(top
, backup
= True)
888 head_tree
= git
.get_commit(head
).get_tree()
889 bottom_tree
= git
.get_commit(bottom
).get_tree()
890 if head_tree
== bottom_tree
:
891 # We must just reparent this patch and create a new commit
893 descr
= patch
.get_description()
894 author_name
= patch
.get_authname()
895 author_email
= patch
.get_authemail()
896 author_date
= patch
.get_authdate()
897 committer_name
= patch
.get_commname()
898 committer_email
= patch
.get_commemail()
900 top_tree
= git
.get_commit(top
).get_tree()
902 top
= git
.commit(message
= descr
, parents
= [head
],
903 cache_update
= False,
906 author_name
= author_name
,
907 author_email
= author_email
,
908 author_date
= author_date
,
909 committer_name
= committer_name
,
910 committer_email
= committer_email
)
912 patch
.set_bottom(head
, backup
= True)
913 patch
.set_top(top
, backup
= True)
915 self
.log_patch(patch
, 'push(f)')
918 # stop the fast-forwarding, must do a real merge
922 unapplied
.remove(name
)
929 append_strings(self
.__applied_file
, names
[0:forwarded
])
931 f
= file(self
.__unapplied_file
, 'w+')
932 f
.writelines([line
+ '\n' for line
in unapplied
])
935 self
.__set_current(name
)
939 def merged_patches(self
, names
):
940 """Test which patches were merged upstream by reverse-applying
941 them in reverse order. The function returns the list of
942 patches detected to have been applied. The state of the tree
943 is restored to the original one
945 patches
= [Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
951 if git
.apply_diff(p
.get_top(), p
.get_bottom()):
952 merged
.append(p
.get_name())
959 def push_patch(self
, name
, empty
= False):
960 """Pushes a patch on the stack
962 unapplied
= self
.get_unapplied()
963 assert(name
in unapplied
)
965 self
.__begin_stack_check()
967 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
969 head
= git
.get_head()
970 bottom
= patch
.get_bottom()
971 top
= patch
.get_top()
976 # top != bottom always since we have a commit for each patch
978 # just make an empty patch (top = bottom = HEAD). This
979 # option is useful to allow undoing already merged
980 # patches. The top is updated by refresh_patch since we
981 # need an empty commit
982 patch
.set_bottom(head
, backup
= True)
983 patch
.set_top(head
, backup
= True)
986 # reset the backup information. No need for logging
987 patch
.set_bottom(bottom
, backup
= True)
988 patch
.set_top(top
, backup
= True)
992 # new patch needs to be refreshed.
993 # The current patch is empty after merge.
994 patch
.set_bottom(head
, backup
= True)
995 patch
.set_top(head
, backup
= True)
997 # Try the fast applying first. If this fails, fall back to the
999 if not git
.apply_diff(bottom
, top
):
1000 # if git.apply_diff() fails, the patch requires a diff3
1001 # merge and can be reported as modified
1004 # merge can fail but the patch needs to be pushed
1006 git
.merge(bottom
, head
, top
, recursive
= True)
1007 except git
.GitException
, ex
:
1008 print >> sys
.stderr
, \
1009 'The merge failed during "push". ' \
1010 'Use "refresh" after fixing the conflicts'
1012 append_string(self
.__applied_file
, name
)
1014 unapplied
.remove(name
)
1015 f
= file(self
.__unapplied_file
, 'w+')
1016 f
.writelines([line
+ '\n' for line
in unapplied
])
1019 self
.__set_current(name
)
1021 # head == bottom case doesn't need to refresh the patch
1022 if empty
or head
!= bottom
:
1024 # if the merge was OK and no conflicts, just refresh the patch
1025 # The GIT cache was already updated by the merge operation
1030 self
.refresh_patch(cache_update
= False, log
= log
)
1032 # we store the correctly merged files only for
1033 # tracking the conflict history. Note that the
1034 # git.merge() operations shouls always leave the index
1035 # in a valid state (i.e. only stage 0 files)
1036 self
.refresh_patch(cache_update
= False, log
= 'push(c)')
1037 raise StackException
, str(ex
)
1041 def undo_push(self
):
1042 name
= self
.get_current()
1045 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
1046 old_bottom
= patch
.get_old_bottom()
1047 old_top
= patch
.get_old_top()
1049 # the top of the patch is changed by a push operation only
1050 # together with the bottom (otherwise the top was probably
1051 # modified by 'refresh'). If they are both unchanged, there
1052 # was a fast forward
1053 if old_bottom
== patch
.get_bottom() and old_top
!= patch
.get_top():
1054 raise StackException
, 'No undo information available'
1057 self
.pop_patch(name
)
1058 ret
= patch
.restore_old_boundaries()
1060 self
.log_patch(patch
, 'undo')
1064 def pop_patch(self
, name
, keep
= False):
1065 """Pops the top patch from the stack
1067 applied
= self
.get_applied()
1069 assert(name
in applied
)
1071 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
1073 # only keep the local changes
1074 if keep
and not git
.apply_diff(git
.get_head(), patch
.get_bottom()):
1075 raise StackException
, \
1076 'Failed to pop patches while preserving the local changes'
1078 git
.switch(patch
.get_bottom(), keep
)
1080 # save the new applied list
1081 idx
= applied
.index(name
) + 1
1083 popped
= applied
[:idx
]
1085 unapplied
= popped
+ self
.get_unapplied()
1087 f
= file(self
.__unapplied_file
, 'w+')
1088 f
.writelines([line
+ '\n' for line
in unapplied
])
1094 f
= file(self
.__applied_file
, 'w+')
1095 f
.writelines([line
+ '\n' for line
in applied
])
1099 self
.__set_current(None)
1101 self
.__set_current(applied
[-1])
1103 self
.__end_stack_check()
1105 def empty_patch(self
, name
):
1106 """Returns True if the patch is empty
1108 self
.__patch_name_valid(name
)
1109 patch
= Patch(name
, self
.__patch_dir
, self
.__refs_dir
)
1110 bottom
= patch
.get_bottom()
1111 top
= patch
.get_top()
1115 elif git
.get_commit(top
).get_tree() \
1116 == git
.get_commit(bottom
).get_tree():
1121 def rename_patch(self
, oldname
, newname
):
1122 self
.__patch_name_valid(newname
)
1124 applied
= self
.get_applied()
1125 unapplied
= self
.get_unapplied()
1127 if oldname
== newname
:
1128 raise StackException
, '"To" name and "from" name are the same'
1130 if newname
in applied
or newname
in unapplied
:
1131 raise StackException
, 'Patch "%s" already exists' % newname
1133 if self
.patch_hidden(oldname
):
1134 self
.unhide_patch(oldname
)
1135 self
.hide_patch(newname
)
1137 if oldname
in unapplied
:
1138 Patch(oldname
, self
.__patch_dir
, self
.__refs_dir
).rename(newname
)
1139 unapplied
[unapplied
.index(oldname
)] = newname
1141 f
= file(self
.__unapplied_file
, 'w+')
1142 f
.writelines([line
+ '\n' for line
in unapplied
])
1144 elif oldname
in applied
:
1145 Patch(oldname
, self
.__patch_dir
, self
.__refs_dir
).rename(newname
)
1146 if oldname
== self
.get_current():
1147 self
.__set_current(newname
)
1149 applied
[applied
.index(oldname
)] = newname
1151 f
= file(self
.__applied_file
, 'w+')
1152 f
.writelines([line
+ '\n' for line
in applied
])
1155 raise StackException
, 'Unknown patch "%s"' % oldname
1157 def log_patch(self
, patch
, message
):
1158 """Generate a log commit for a patch
1160 top
= git
.get_commit(patch
.get_top())
1161 msg
= '%s\t%s' %
(message
, top
.get_id_hash())
1163 old_log
= patch
.get_log()
1169 log
= git
.commit(message
= msg
, parents
= parents
,
1170 cache_update
= False, tree_id
= top
.get_tree(),
1174 def hide_patch(self
, name
):
1175 """Add the patch to the hidden list.
1177 if not self
.patch_exists(name
):
1178 raise StackException
, 'Unknown patch "%s"' % name
1179 elif self
.patch_hidden(name
):
1180 raise StackException
, 'Patch "%s" already hidden' % name
1182 append_string(self
.__hidden_file
, name
)
1184 def unhide_patch(self
, name
):
1185 """Add the patch to the hidden list.
1187 if not self
.patch_exists(name
):
1188 raise StackException
, 'Unknown patch "%s"' % name
1189 hidden
= self
.get_hidden()
1190 if not name
in hidden
:
1191 raise StackException
, 'Patch "%s" not hidden' % name
1195 f
= file(self
.__hidden_file
, 'w+')
1196 f
.writelines([line
+ '\n' for line
in hidden
])