1 # -*- coding: utf-8 -*-
2 """Basic quilt-like functionality"""
4 from __future__
import absolute_import
, division
, print_function
5 from email
.utils
import formatdate
9 from stgit
import git
, basedir
, templates
10 from stgit
.config
import config
11 from stgit
.exception
import StackException
12 from stgit
.lib
import git
as libgit
, stackupgrade
13 from stgit
.out
import out
14 from stgit
.run
import Run
15 from stgit
.utils
import (add_sign_line
,
29 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
31 This program is free software; you can redistribute it and/or modify
32 it under the terms of the GNU General Public License version 2 as
33 published by the Free Software Foundation.
35 This program is distributed in the hope that it will be useful,
36 but WITHOUT ANY WARRANTY; without even the implied warranty of
37 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38 GNU General Public License for more details.
40 You should have received a copy of the GNU General Public License
41 along with this program; if not, see http://www.gnu.org/licenses/.
45 class FilterUntil(object):
47 self
.should_print
= True
48 def __call__(self
, x
, until_test
, prefix
):
50 self
.should_print
= False
52 return x
[0:len(prefix
)] != prefix
58 __comment_prefix
= 'STG:'
59 __patch_prefix
= 'STG_PATCH:'
61 def __clean_comments(f
):
62 """Removes lines marked for status in a commit file
66 # remove status-prefixed lines
69 patch_filter
= FilterUntil()
70 until_test
= lambda t
: t
== (__patch_prefix
+ '\n')
71 lines
= [l
for l
in lines
if patch_filter(l
, until_test
, __comment_prefix
)]
73 # remove empty lines at the end
74 while len(lines
) != 0 and lines
[-1] == '\n':
81 # TODO: move this out of the stgit.stack module, it is really for
82 # higher level commands to handle the user interaction
83 def edit_file(series
, line
, comment
, show_patch
= True):
84 fname
= '.stgitmsg.txt'
85 tmpl
= templates
.get_template('patchdescr.tmpl')
87 with
open(fname
, 'w+') as f
:
91 print(tmpl
, end
=' ', file=f
)
94 print(__comment_prefix
, comment
, file=f
)
95 print(__comment_prefix
,
96 'Lines prefixed with "%s" will be automatically removed.'
97 % __comment_prefix
, file=f
)
98 print(__comment_prefix
,
99 'Trailing empty lines will be automatically removed.', file=f
)
102 print(__patch_prefix
, file=f
)
103 # series.get_patch(series.get_current()).get_top()
104 diff_str
= git
.diff(rev1
= series
.get_patch(series
.get_current()).get_bottom())
107 #Vim modeline must be near the end.
108 print(__comment_prefix
, 'vi: set textwidth=75 filetype=diff nobackup:', file=f
)
112 with
open(fname
, 'r+') as f
:
125 class StgitObject(object):
126 """An object with stgit-like properties stored as files in a directory
128 def _set_dir(self
, dir):
133 def create_empty_field(self
, name
):
134 create_empty_file(os
.path
.join(self
.__dir
, name
))
136 def _get_field(self
, name
, multiline
= False):
137 id_file
= os
.path
.join(self
.__dir
, name
)
138 if os
.path
.isfile(id_file
):
139 line
= read_string(id_file
, multiline
)
147 def _set_field(self
, name
, value
, multiline
= False):
148 fname
= os
.path
.join(self
.__dir
, name
)
149 if value
and value
!= '':
150 write_string(fname
, value
, multiline
)
151 elif os
.path
.isfile(fname
):
155 class Patch(StgitObject
):
156 """Basic patch implementation
158 def __init_refs(self
):
159 self
.__top
_ref
= self
.__refs
_base
+ '/' + self
.__name
160 self
.__log
_ref
= self
.__top
_ref
+ '.log'
162 def __init__(self
, name
, series_dir
, refs_base
):
163 self
.__series
_dir
= series_dir
165 self
._set
_dir
(os
.path
.join(self
.__series
_dir
, self
.__name
))
166 self
.__refs
_base
= refs_base
170 os
.mkdir(self
._dir
())
172 def delete(self
, keep_log
= False):
173 if os
.path
.isdir(self
._dir
()):
174 for f
in os
.listdir(self
._dir
()):
175 os
.remove(os
.path
.join(self
._dir
(), f
))
176 os
.rmdir(self
._dir
())
178 out
.warn('Patch directory "%s" does not exist' % self
._dir
())
180 # the reference might not exist if the repository was corrupted
181 git
.delete_ref(self
.__top
_ref
)
182 except git
.GitException
as e
:
184 if not keep_log
and git
.ref_exists(self
.__log
_ref
):
185 git
.delete_ref(self
.__log
_ref
)
190 def rename(self
, newname
):
192 old_top_ref
= self
.__top
_ref
193 old_log_ref
= self
.__log
_ref
194 self
.__name
= newname
195 self
._set
_dir
(os
.path
.join(self
.__series
_dir
, self
.__name
))
198 git
.rename_ref(old_top_ref
, self
.__top
_ref
)
199 if git
.ref_exists(old_log_ref
):
200 git
.rename_ref(old_log_ref
, self
.__log
_ref
)
201 os
.rename(olddir
, self
._dir
())
203 def __update_top_ref(self
, ref
):
204 git
.set_ref(self
.__top
_ref
, ref
)
205 self
._set
_field
('top', ref
)
206 self
._set
_field
('bottom', git
.get_commit(ref
).get_parent())
208 def __update_log_ref(self
, ref
):
209 git
.set_ref(self
.__log
_ref
, ref
)
211 def get_old_bottom(self
):
212 return git
.get_commit(self
.get_old_top()).get_parent()
214 def get_bottom(self
):
215 return git
.get_commit(self
.get_top()).get_parent()
217 def get_old_top(self
):
218 return self
._get
_field
('top.old')
221 return git
.rev_parse(self
.__top
_ref
)
223 def set_top(self
, value
, backup
= False):
225 curr_top
= self
.get_top()
226 self
._set
_field
('top.old', curr_top
)
227 self
._set
_field
('bottom.old', git
.get_commit(curr_top
).get_parent())
228 self
.__update
_top
_ref
(value
)
230 def restore_old_boundaries(self
):
231 top
= self
._get
_field
('top.old')
234 self
.__update
_top
_ref
(top
)
239 def get_description(self
):
240 return self
._get
_field
('description', True)
242 def set_description(self
, line
):
243 self
._set
_field
('description', line
, True)
245 def get_authname(self
):
246 return self
._get
_field
('authname')
248 def set_authname(self
, name
):
249 self
._set
_field
('authname', name
or git
.author().name
)
251 def get_authemail(self
):
252 return self
._get
_field
('authemail')
254 def set_authemail(self
, email
):
255 self
._set
_field
('authemail', email
or git
.author().email
)
257 def get_authdate(self
):
258 date
= self
._get
_field
('authdate')
262 if re
.match(r
'[0-9]+\s+[+-][0-9]+', date
):
263 # Unix time (seconds) + time zone
264 secs_tz
= date
.split()
265 date
= formatdate(int(secs_tz
[0]))[:-5] + secs_tz
[1]
269 def set_authdate(self
, date
):
270 self
._set
_field
('authdate', date
or git
.author().date
)
272 def get_commname(self
):
273 return self
._get
_field
('commname')
275 def set_commname(self
, name
):
276 self
._set
_field
('commname', name
or git
.committer().name
)
278 def get_commemail(self
):
279 return self
._get
_field
('commemail')
281 def set_commemail(self
, email
):
282 self
._set
_field
('commemail', email
or git
.committer().email
)
285 return self
._get
_field
('log')
287 def set_log(self
, value
, backup
= False):
288 self
._set
_field
('log', value
)
289 self
.__update
_log
_ref
(value
)
291 class PatchSet(StgitObject
):
292 def __init__(self
, name
= None):
297 self
.set_name (git
.get_head_file())
298 self
.__base
_dir
= basedir
.get()
299 except git
.GitException
as ex
:
300 raise StackException('GIT tree not initialised: %s' % ex
)
302 self
._set
_dir
(os
.path
.join(self
.__base
_dir
, 'patches', self
.get_name()))
306 def set_name(self
, name
):
310 return self
.__base
_dir
313 """Return the head of the branch
315 crt
= self
.get_current_patch()
319 return self
.get_base()
321 def get_protected(self
):
322 return os
.path
.isfile(os
.path
.join(self
._dir
(), 'protected'))
325 protect_file
= os
.path
.join(self
._dir
(), 'protected')
326 if not os
.path
.isfile(protect_file
):
327 create_empty_file(protect_file
)
330 protect_file
= os
.path
.join(self
._dir
(), 'protected')
331 if os
.path
.isfile(protect_file
):
332 os
.remove(protect_file
)
334 def __branch_descr(self
):
335 return 'branch.%s.description' % self
.get_name()
337 def get_description(self
):
338 return config
.get(self
.__branch
_descr
()) or ''
340 def set_description(self
, line
):
342 config
.set(self
.__branch
_descr
(), line
)
344 config
.unset(self
.__branch
_descr
())
346 def head_top_equal(self
):
347 """Return true if the head and the top are the same
349 crt
= self
.get_current_patch()
351 # we don't care, no patches applied
353 return git
.get_head() == crt
.get_top()
355 def is_initialised(self
):
356 """Checks if series is already initialised
358 return config
.get(stackupgrade
.format_version_key(self
.get_name())
362 def shortlog(patches
):
363 log
= ''.join(Run('git', 'log', '--pretty=short',
364 p
.get_top(), '^%s' % p
.get_bottom()).raw_output()
366 return Run('git', 'shortlog').raw_input(log
).raw_output()
368 class Series(PatchSet
):
369 """Class including the operations on series
371 def __init__(self
, name
= None):
372 """Takes a series name as the parameter.
374 PatchSet
.__init
__(self
, name
)
376 # Update the branch to the latest format version if it is
377 # initialized, but don't touch it if it isn't.
378 stackupgrade
.update_to_current_format_version(
379 libgit
.Repository
.default(), self
.get_name())
381 self
.__refs
_base
= 'refs/patches/%s' % self
.get_name()
383 self
.__applied
_file
= os
.path
.join(self
._dir
(), 'applied')
384 self
.__unapplied
_file
= os
.path
.join(self
._dir
(), 'unapplied')
385 self
.__hidden
_file
= os
.path
.join(self
._dir
(), 'hidden')
387 # where this series keeps its patches
388 self
.__patch
_dir
= os
.path
.join(self
._dir
(), 'patches')
391 self
.__trash
_dir
= os
.path
.join(self
._dir
(), 'trash')
393 def __patch_name_valid(self
, name
):
394 """Raise an exception if the patch name is not valid.
396 if not name
or re
.search(r
'[^\w.-]', name
):
397 raise StackException('Invalid patch name: "%s"' % name
)
399 def get_patch(self
, name
):
400 """Return a Patch object for the given name
402 return Patch(name
, self
.__patch
_dir
, self
.__refs
_base
)
404 def get_current_patch(self
):
405 """Return a Patch object representing the topmost patch, or
406 None if there is no such patch."""
407 crt
= self
.get_current()
410 return self
.get_patch(crt
)
412 def get_current(self
):
413 """Return the name of the topmost patch, or None if there is
416 applied
= self
.get_applied()
417 except StackException
:
418 # No "applied" file: branch is not initialized.
423 # No patches applied.
426 def get_applied(self
):
427 if not os
.path
.isfile(self
.__applied
_file
):
428 raise StackException('Branch "%s" not initialised' %
430 return read_strings(self
.__applied
_file
)
432 def set_applied(self
, applied
):
433 write_strings(self
.__applied
_file
, applied
)
435 def get_unapplied(self
):
436 if not os
.path
.isfile(self
.__unapplied
_file
):
437 raise StackException('Branch "%s" not initialised' %
439 return read_strings(self
.__unapplied
_file
)
441 def set_unapplied(self
, unapplied
):
442 write_strings(self
.__unapplied
_file
, unapplied
)
444 def get_hidden(self
):
445 if not os
.path
.isfile(self
.__hidden
_file
):
447 return read_strings(self
.__hidden
_file
)
449 def set_hidden(self
, hidden
):
450 write_strings(self
.__hidden
_file
, hidden
)
453 # Return the parent of the bottommost patch, if there is one.
454 if os
.path
.isfile(self
.__applied
_file
):
455 with
open(self
.__applied
_file
) as f
:
456 bottommost
= f
.readline().strip()
458 return self
.get_patch(bottommost
).get_bottom()
459 # No bottommost patch, so just return HEAD
460 return git
.get_head()
462 def get_parent_remote(self
):
463 value
= config
.get('branch.%s.remote' % self
.get_name())
466 elif 'origin' in git
.remotes_list():
467 out
.note(('No parent remote declared for stack "%s",'
468 ' defaulting to "origin".' % self
.get_name()),
469 ('Consider setting "branch.%s.remote" and'
470 ' "branch.%s.merge" with "git config".'
471 % (self
.get_name(), self
.get_name())))
474 raise StackException('Cannot find a parent remote for "%s"' %
477 def __set_parent_remote(self
, remote
):
478 value
= config
.set('branch.%s.remote' % self
.get_name(), remote
)
480 def get_parent_branch(self
):
481 value
= config
.get('branch.%s.stgit.parentbranch' % self
.get_name())
484 elif git
.rev_parse('heads/origin'):
485 out
.note(('No parent branch declared for stack "%s",'
486 ' defaulting to "heads/origin".' % self
.get_name()),
487 ('Consider setting "branch.%s.stgit.parentbranch"'
488 ' with "git config".' % self
.get_name()))
489 return 'heads/origin'
491 raise StackException('Cannot find a parent branch for "%s"' %
494 def __set_parent_branch(self
, name
):
495 if config
.get('branch.%s.remote' % self
.get_name()):
496 # Never set merge if remote is not set to avoid
497 # possibly-erroneous lookups into 'origin'
498 config
.set('branch.%s.merge' % self
.get_name(), name
)
499 config
.set('branch.%s.stgit.parentbranch' % self
.get_name(), name
)
501 def set_parent(self
, remote
, localbranch
):
504 self
.__set
_parent
_remote
(remote
)
505 self
.__set
_parent
_branch
(localbranch
)
506 # We'll enforce this later
508 # raise StackException('Parent branch (%s) should be specified for %s' %
509 # localbranch, self.get_name())
511 def __patch_is_current(self
, patch
):
512 return patch
.get_name() == self
.get_current()
514 def patch_applied(self
, name
):
515 """Return true if the patch exists in the applied list
517 return name
in self
.get_applied()
519 def patch_unapplied(self
, name
):
520 """Return true if the patch exists in the unapplied list
522 return name
in self
.get_unapplied()
524 def patch_hidden(self
, name
):
525 """Return true if the patch is hidden.
527 return name
in self
.get_hidden()
529 def patch_exists(self
, name
):
530 """Return true if there is a patch with the given name, false
532 return self
.patch_applied(name
) or self
.patch_unapplied(name
) \
533 or self
.patch_hidden(name
)
535 def init(self
, create_at
=False, parent_remote
=None, parent_branch
=None):
536 """Initialises the stgit series
538 if self
.is_initialised():
539 raise StackException('%s already initialized' % self
.get_name())
540 for d
in [self
._dir
()]:
541 if os
.path
.exists(d
):
542 raise StackException('%s already exists' % d
)
544 if (create_at
!=False):
545 git
.create_branch(self
.get_name(), create_at
)
547 os
.makedirs(self
.__patch
_dir
)
549 self
.set_parent(parent_remote
, parent_branch
)
551 self
.create_empty_field('applied')
552 self
.create_empty_field('unapplied')
554 config
.set(stackupgrade
.format_version_key(self
.get_name()),
555 str(stackupgrade
.FORMAT_VERSION
))
557 def rename(self
, to_name
):
560 to_stack
= Series(to_name
)
562 if to_stack
.is_initialised():
563 raise StackException('"%s" already exists' % to_stack
.get_name())
565 patches
= self
.get_applied() + self
.get_unapplied()
567 git
.rename_branch(self
.get_name(), to_name
)
569 for patch
in patches
:
570 git
.rename_ref('refs/patches/%s/%s' % (self
.get_name(), patch
),
571 'refs/patches/%s/%s' % (to_name
, patch
))
572 git
.rename_ref('refs/patches/%s/%s.log' % (self
.get_name(), patch
),
573 'refs/patches/%s/%s.log' % (to_name
, patch
))
574 if os
.path
.isdir(self
._dir
()):
575 rename(os
.path
.join(self
._basedir
(), 'patches'),
576 self
.get_name(), to_stack
.get_name())
578 # Rename the config section
579 for k
in ['branch.%s', 'branch.%s.stgit']:
580 config
.rename_section(k
% self
.get_name(), k
% to_name
)
582 self
.__init
__(to_name
)
584 def clone(self
, target_series
):
588 # allow cloning of branches not under StGIT control
589 base
= self
.get_base()
591 base
= git
.get_head()
592 Series(target_series
).init(create_at
= base
)
593 new_series
= Series(target_series
)
595 # generate an artificial description file
596 new_series
.set_description('clone of "%s"' % self
.get_name())
598 # clone self's entire series as unapplied patches
600 # allow cloning of branches not under StGIT control
601 applied
= self
.get_applied()
602 unapplied
= self
.get_unapplied()
603 patches
= applied
+ unapplied
606 patches
= applied
= unapplied
= []
608 patch
= self
.get_patch(p
)
609 newpatch
= new_series
.new_patch(p
, message
= patch
.get_description(),
610 can_edit
= False, unapplied
= True,
611 bottom
= patch
.get_bottom(),
612 top
= patch
.get_top(),
613 author_name
= patch
.get_authname(),
614 author_email
= patch
.get_authemail(),
615 author_date
= patch
.get_authdate())
617 out
.info('Setting log to %s' % patch
.get_log())
618 newpatch
.set_log(patch
.get_log())
620 out
.info('No log for %s' % p
)
622 # fast forward the cloned series to self's top
623 new_series
.forward_patches(applied
)
625 # Clone parent informations
626 value
= config
.get('branch.%s.remote' % self
.get_name())
628 config
.set('branch.%s.remote' % target_series
, value
)
630 value
= config
.get('branch.%s.merge' % self
.get_name())
632 config
.set('branch.%s.merge' % target_series
, value
)
634 value
= config
.get('branch.%s.stgit.parentbranch' % self
.get_name())
636 config
.set('branch.%s.stgit.parentbranch' % target_series
, value
)
638 def delete(self
, force
= False, cleanup
= False):
639 """Deletes an stgit series
641 if self
.is_initialised():
642 patches
= self
.get_unapplied() + self
.get_applied() + \
644 if not force
and patches
:
645 raise StackException(
646 'Cannot %s: the series still contains patches' %
647 ('delete', 'clean up')[cleanup
])
649 self
.get_patch(p
).delete()
651 # remove the trash directory if any
652 if os
.path
.exists(self
.__trash
_dir
):
653 for fname
in os
.listdir(self
.__trash
_dir
):
654 os
.remove(os
.path
.join(self
.__trash
_dir
, fname
))
655 os
.rmdir(self
.__trash
_dir
)
657 # FIXME: find a way to get rid of those manual removals
658 # (move functionality to StgitObject ?)
659 if os
.path
.exists(self
.__applied
_file
):
660 os
.remove(self
.__applied
_file
)
661 if os
.path
.exists(self
.__unapplied
_file
):
662 os
.remove(self
.__unapplied
_file
)
663 if os
.path
.exists(self
.__hidden
_file
):
664 os
.remove(self
.__hidden
_file
)
665 if os
.path
.exists(self
._dir
()+'/orig-base'):
666 os
.remove(self
._dir
()+'/orig-base')
668 if not os
.listdir(self
.__patch
_dir
):
669 os
.rmdir(self
.__patch
_dir
)
671 out
.warn('Patch directory %s is not empty' % self
.__patch
_dir
)
674 os
.removedirs(self
._dir
())
676 raise StackException('Series directory %s is not empty'
681 git
.delete_branch(self
.get_name())
682 except git
.GitException
:
683 out
.warn('Could not delete branch "%s"' % self
.get_name())
684 config
.remove_section('branch.%s' % self
.get_name())
686 config
.remove_section('branch.%s.stgit' % self
.get_name())
688 def refresh_patch(self
, files
= None, message
= None, edit
= False,
692 author_name
= None, author_email
= None,
694 committer_name
= None, committer_email
= None,
695 backup
= True, sign_str
= None, log
= 'refresh',
696 notes
= None, bottom
= None):
697 """Generates a new commit for the topmost patch
699 patch
= self
.get_current_patch()
701 raise StackException('No patches applied')
703 descr
= patch
.get_description()
704 if not (message
or descr
):
710 # TODO: move this out of the stgit.stack module, it is really
711 # for higher level commands to handle the user interaction
712 if not message
and edit
:
713 descr
= edit_file(self
, descr
.rstrip(), \
714 'Please edit the description for patch "%s" ' \
715 'above.' % patch
.get_name(), show_patch
)
718 author_name
= patch
.get_authname()
720 author_email
= patch
.get_authemail()
721 if not committer_name
:
722 committer_name
= patch
.get_commname()
723 if not committer_email
:
724 committer_email
= patch
.get_commemail()
726 descr
= add_sign_line(descr
, sign_str
, committer_name
, committer_email
)
729 bottom
= patch
.get_bottom()
732 tree_id
= git
.get_commit(bottom
).get_tree()
736 commit_id
= git
.commit(files
= files
,
737 message
= descr
, parents
= [bottom
],
738 cache_update
= cache_update
,
742 author_name
= author_name
,
743 author_email
= author_email
,
744 author_date
= author_date
,
745 committer_name
= committer_name
,
746 committer_email
= committer_email
)
748 patch
.set_top(commit_id
, backup
= backup
)
749 patch
.set_description(descr
)
750 patch
.set_authname(author_name
)
751 patch
.set_authemail(author_email
)
752 patch
.set_authdate(author_date
)
753 patch
.set_commname(committer_name
)
754 patch
.set_commemail(committer_email
)
757 self
.log_patch(patch
, log
, notes
)
761 def new_patch(self
, name
, message
= None, can_edit
= True,
762 unapplied
= False, show_patch
= False,
763 top
= None, bottom
= None, commit
= True,
764 author_name
= None, author_email
= None, author_date
= None,
765 committer_name
= None, committer_email
= None,
766 before_existing
= False, sign_str
= None):
767 """Creates a new patch, either pointing to an existing commit object,
768 or by creating a new commit object.
771 assert commit
or (top
and bottom
)
772 assert not before_existing
or (top
and bottom
)
773 assert not (commit
and before_existing
)
774 assert (top
and bottom
) or (not top
and not bottom
)
775 assert commit
or (not top
or (bottom
== git
.get_commit(top
).get_parent()))
778 self
.__patch
_name
_valid
(name
)
779 if self
.patch_exists(name
):
780 raise StackException('Patch "%s" already exists' % name
)
782 # TODO: move this out of the stgit.stack module, it is really
783 # for higher level commands to handle the user interaction
785 return add_sign_line(msg
, sign_str
,
786 committer_name
or git
.committer().name
,
787 committer_email
or git
.committer().email
)
788 if not message
and can_edit
:
791 'Please enter the description for the patch above.',
794 descr
= sign(message
)
796 head
= git
.get_head()
799 name
= make_patch_name(descr
, self
.patch_exists
)
801 patch
= self
.get_patch(name
)
804 patch
.set_description(descr
)
805 patch
.set_authname(author_name
)
806 patch
.set_authemail(author_email
)
807 patch
.set_authdate(author_date
)
808 patch
.set_commname(committer_name
)
809 patch
.set_commemail(committer_email
)
812 insert_string(self
.__applied
_file
, patch
.get_name())
814 patches
= [patch
.get_name()] + self
.get_unapplied()
815 write_strings(self
.__unapplied
_file
, patches
)
818 append_string(self
.__applied
_file
, patch
.get_name())
823 top_commit
= git
.get_commit(top
)
826 top_commit
= git
.get_commit(head
)
828 # create a commit for the patch (may be empty if top == bottom);
829 # only commit on top of the current branch
830 assert(unapplied
or bottom
== head
)
831 commit_id
= git
.commit(message
= descr
, parents
= [bottom
],
832 cache_update
= False,
833 tree_id
= top_commit
.get_tree(),
834 allowempty
= True, set_head
= set_head
,
835 author_name
= author_name
,
836 author_email
= author_email
,
837 author_date
= author_date
,
838 committer_name
= committer_name
,
839 committer_email
= committer_email
)
840 # set the patch top to the new commit
841 patch
.set_top(commit_id
)
845 self
.log_patch(patch
, 'new')
849 def delete_patch(self
, name
, keep_log
= False):
852 self
.__patch
_name
_valid
(name
)
853 patch
= self
.get_patch(name
)
855 if self
.__patch
_is
_current
(patch
):
857 elif self
.patch_applied(name
):
858 raise StackException('Cannot remove an applied patch, "%s", '
859 'which is not current' % name
)
860 elif name
not in self
.get_unapplied():
861 raise StackException('Unknown patch "%s"' % name
)
863 # save the commit id to a trash file
864 write_string(os
.path
.join(self
.__trash
_dir
, name
), patch
.get_top())
866 patch
.delete(keep_log
= keep_log
)
868 unapplied
= self
.get_unapplied()
869 unapplied
.remove(name
)
870 write_strings(self
.__unapplied
_file
, unapplied
)
872 def forward_patches(self
, names
):
873 """Try to fast-forward an array of patches.
875 On return, patches in names[0:returned_value] have been pushed on the
876 stack. Apply the rest with push_patch
878 unapplied
= self
.get_unapplied()
884 assert(name
in unapplied
)
886 patch
= self
.get_patch(name
)
889 bottom
= patch
.get_bottom()
890 top
= patch
.get_top()
892 # top != bottom always since we have a commit for each patch
894 # reset the backup information. No logging since the
895 # patch hasn't changed
896 patch
.set_top(top
, backup
= True)
899 head_tree
= git
.get_commit(head
).get_tree()
900 bottom_tree
= git
.get_commit(bottom
).get_tree()
901 if head_tree
== bottom_tree
:
902 # We must just reparent this patch and create a new commit
904 descr
= patch
.get_description()
905 author_name
= patch
.get_authname()
906 author_email
= patch
.get_authemail()
907 author_date
= patch
.get_authdate()
908 committer_name
= patch
.get_commname()
909 committer_email
= patch
.get_commemail()
911 top_tree
= git
.get_commit(top
).get_tree()
913 top
= git
.commit(message
= descr
, parents
= [head
],
914 cache_update
= False,
917 author_name
= author_name
,
918 author_email
= author_email
,
919 author_date
= author_date
,
920 committer_name
= committer_name
,
921 committer_email
= committer_email
)
923 patch
.set_top(top
, backup
= True)
925 self
.log_patch(patch
, 'push(f)')
928 # stop the fast-forwarding, must do a real merge
932 unapplied
.remove(name
)
939 append_strings(self
.__applied
_file
, names
[0:forwarded
])
940 write_strings(self
.__unapplied
_file
, unapplied
)
944 def merged_patches(self
, names
):
945 """Test which patches were merged upstream by reverse-applying
946 them in reverse order. The function returns the list of
947 patches detected to have been applied. The state of the tree
948 is restored to the original one
950 patches
= [self
.get_patch(name
) for name
in names
]
955 if git
.apply_diff(p
.get_top(), p
.get_bottom()):
956 merged
.append(p
.get_name())
963 def push_empty_patch(self
, name
):
964 """Pushes an empty patch on the stack
966 unapplied
= self
.get_unapplied()
967 assert(name
in unapplied
)
969 # patch = self.get_patch(name)
970 head
= git
.get_head()
972 append_string(self
.__applied
_file
, name
)
974 unapplied
.remove(name
)
975 write_strings(self
.__unapplied
_file
, unapplied
)
977 self
.refresh_patch(bottom
= head
, cache_update
= False, log
= 'push(m)')
979 def push_patch(self
, name
):
980 """Pushes a patch on the stack
982 unapplied
= self
.get_unapplied()
983 assert(name
in unapplied
)
985 patch
= self
.get_patch(name
)
987 head
= git
.get_head()
988 bottom
= patch
.get_bottom()
989 top
= patch
.get_top()
990 # top != bottom always since we have a commit for each patch
993 # A fast-forward push. Just reset the backup
994 # information. No need for logging
995 patch
.set_top(top
, backup
= True)
998 append_string(self
.__applied
_file
, name
)
1000 unapplied
.remove(name
)
1001 write_strings(self
.__unapplied
_file
, unapplied
)
1004 # Need to create a new commit an merge in the old patch
1008 # Try the fast applying first. If this fails, fall back to the
1010 if not git
.apply_diff(bottom
, top
):
1011 # if git.apply_diff() fails, the patch requires a diff3
1012 # merge and can be reported as modified
1015 # merge can fail but the patch needs to be pushed
1017 git
.merge_recursive(bottom
, head
, top
)
1018 except git
.GitException
:
1019 out
.error('The merge failed during "push".',
1020 'Revert the operation with "stg undo".')
1022 append_string(self
.__applied
_file
, name
)
1024 unapplied
.remove(name
)
1025 write_strings(self
.__unapplied
_file
, unapplied
)
1028 # if the merge was OK and no conflicts, just refresh the patch
1029 # The GIT cache was already updated by the merge operation
1034 self
.refresh_patch(bottom
= head
, cache_update
= False, log
= log
)
1036 # we make the patch empty, with the merged state in the
1038 self
.refresh_patch(bottom
= head
, cache_update
= False,
1039 empty
= True, log
= 'push(c)')
1040 raise StackException(str(ex
))
1044 def pop_patch(self
, name
, keep
= False):
1045 """Pops the top patch from the stack
1047 applied
= self
.get_applied()
1049 assert(name
in applied
)
1051 patch
= self
.get_patch(name
)
1053 if git
.get_head_file() == self
.get_name():
1054 if keep
and not git
.apply_diff(git
.get_head(), patch
.get_bottom(),
1055 check_index
= False):
1056 raise StackException(
1057 'Failed to pop patches while preserving the local changes')
1058 git
.switch(patch
.get_bottom(), keep
)
1060 git
.set_branch(self
.get_name(), patch
.get_bottom())
1062 # save the new applied list
1063 idx
= applied
.index(name
) + 1
1065 popped
= applied
[:idx
]
1067 unapplied
= popped
+ self
.get_unapplied()
1068 write_strings(self
.__unapplied
_file
, unapplied
)
1072 write_strings(self
.__applied
_file
, applied
)
1074 def empty_patch(self
, name
):
1075 """Returns True if the patch is empty
1077 self
.__patch
_name
_valid
(name
)
1078 patch
= self
.get_patch(name
)
1079 bottom
= patch
.get_bottom()
1080 top
= patch
.get_top()
1084 elif git
.get_commit(top
).get_tree() \
1085 == git
.get_commit(bottom
).get_tree():
1090 def rename_patch(self
, oldname
, newname
):
1091 self
.__patch
_name
_valid
(newname
)
1093 applied
= self
.get_applied()
1094 unapplied
= self
.get_unapplied()
1096 if oldname
== newname
:
1097 raise StackException('"To" name and "from" name are the same')
1099 if newname
in applied
or newname
in unapplied
:
1100 raise StackException('Patch "%s" already exists' % newname
)
1102 if oldname
in unapplied
:
1103 self
.get_patch(oldname
).rename(newname
)
1104 unapplied
[unapplied
.index(oldname
)] = newname
1105 write_strings(self
.__unapplied
_file
, unapplied
)
1106 elif oldname
in applied
:
1107 self
.get_patch(oldname
).rename(newname
)
1109 applied
[applied
.index(oldname
)] = newname
1110 write_strings(self
.__applied
_file
, applied
)
1112 raise StackException('Unknown patch "%s"' % oldname
)
1114 def log_patch(self
, patch
, message
, notes
= None):
1115 """Generate a log commit for a patch
1117 top
= git
.get_commit(patch
.get_top())
1118 old_log
= patch
.get_log()
1121 # replace the current log entry
1123 raise StackException('No log entry to annotate for patch "%s"'
1126 log_commit
= git
.get_commit(old_log
)
1127 msg
= log_commit
.get_log().split('\n')[0]
1128 log_parent
= log_commit
.get_parent()
1130 parents
= [log_parent
]
1134 # generate a new log entry
1136 msg
= '%s\t%s' % (message
, top
.get_id_hash())
1143 msg
+= '\n\n' + notes
1145 log
= git
.commit(message
= msg
, parents
= parents
,
1146 cache_update
= False, tree_id
= top
.get_tree(),