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
.out
import *
25 from stgit
import git
, basedir
, templates
26 from stgit
.config
import config
27 from shutil
import copyfile
30 # stack exception class
31 class StackException(Exception):
36 self
.should_print
= True
37 def __call__(self
, x
, until_test
, prefix
):
39 self
.should_print
= False
41 return x
[0:len(prefix
)] != prefix
47 __comment_prefix
= 'STG:'
48 __patch_prefix
= 'STG_PATCH:'
50 def __clean_comments(f
):
51 """Removes lines marked for status in a commit file
55 # remove status-prefixed lines
58 patch_filter
= FilterUntil()
59 until_test
= lambda t
: t
== (__patch_prefix
+ '\n')
60 lines
= [l
for l
in lines
if patch_filter(l
, until_test
, __comment_prefix
)]
62 # remove empty lines at the end
63 while len(lines
) != 0 and lines
[-1] == '\n':
66 f
.seek(0); f
.truncate()
69 def edit_file(series
, line
, comment
, show_patch
= True):
70 fname
= '.stgitmsg.txt'
71 tmpl
= templates
.get_template('patchdescr.tmpl')
80 print >> f
, __comment_prefix
, comment
81 print >> f
, __comment_prefix
, \
82 'Lines prefixed with "%s" will be automatically removed.' \
84 print >> f
, __comment_prefix
, \
85 'Trailing empty lines will be automatically removed.'
88 print >> f
, __patch_prefix
89 # series.get_patch(series.get_current()).get_top()
90 diff_str
= git
.diff(rev1
= series
.get_patch(series
.get_current()).get_bottom())
93 #Vim modeline must be near the end.
94 print >> f
, __comment_prefix
, 'vi: set textwidth=75 filetype=diff nobackup:'
115 """An object with stgit-like properties stored as files in a directory
117 def _set_dir(self
, dir):
122 def create_empty_field(self
, name
):
123 create_empty_file(os
.path
.join(self
.__dir
, name
))
125 def _get_field(self
, name
, multiline
= False):
126 id_file
= os
.path
.join(self
.__dir
, name
)
127 if os
.path
.isfile(id_file
):
128 line
= read_string(id_file
, multiline
)
136 def _set_field(self
, name
, value
, multiline
= False):
137 fname
= os
.path
.join(self
.__dir
, name
)
138 if value
and value
!= '':
139 write_string(fname
, value
, multiline
)
140 elif os
.path
.isfile(fname
):
144 class Patch(StgitObject
):
145 """Basic patch implementation
147 def __init_refs(self
):
148 self
.__top
_ref
= self
.__refs
_base
+ '/' + self
.__name
149 self
.__log
_ref
= self
.__top
_ref
+ '.log'
151 def __init__(self
, name
, series_dir
, refs_base
):
152 self
.__series
_dir
= series_dir
154 self
._set
_dir
(os
.path
.join(self
.__series
_dir
, self
.__name
))
155 self
.__refs
_base
= refs_base
159 os
.mkdir(self
._dir
())
160 self
.create_empty_field('bottom')
161 self
.create_empty_field('top')
164 for f
in os
.listdir(self
._dir
()):
165 os
.remove(os
.path
.join(self
._dir
(), f
))
166 os
.rmdir(self
._dir
())
167 git
.delete_ref(self
.__top
_ref
)
168 if git
.ref_exists(self
.__log
_ref
):
169 git
.delete_ref(self
.__log
_ref
)
174 def rename(self
, newname
):
176 old_top_ref
= self
.__top
_ref
177 old_log_ref
= self
.__log
_ref
178 self
.__name
= newname
179 self
._set
_dir
(os
.path
.join(self
.__series
_dir
, self
.__name
))
182 git
.rename_ref(old_top_ref
, self
.__top
_ref
)
183 if git
.ref_exists(old_log_ref
):
184 git
.rename_ref(old_log_ref
, self
.__log
_ref
)
185 os
.rename(olddir
, self
._dir
())
187 def __update_top_ref(self
, ref
):
188 git
.set_ref(self
.__top
_ref
, ref
)
190 def __update_log_ref(self
, ref
):
191 git
.set_ref(self
.__log
_ref
, ref
)
193 def update_top_ref(self
):
196 self
.__update
_top
_ref
(top
)
198 def get_old_bottom(self
):
199 return self
._get
_field
('bottom.old')
201 def get_bottom(self
):
202 return self
._get
_field
('bottom')
204 def set_bottom(self
, value
, backup
= False):
206 curr
= self
._get
_field
('bottom')
207 self
._set
_field
('bottom.old', curr
)
208 self
._set
_field
('bottom', value
)
210 def get_old_top(self
):
211 return self
._get
_field
('top.old')
214 return self
._get
_field
('top')
216 def set_top(self
, value
, backup
= False):
218 curr
= self
._get
_field
('top')
219 self
._set
_field
('top.old', curr
)
220 self
._set
_field
('top', value
)
221 self
.__update
_top
_ref
(value
)
223 def restore_old_boundaries(self
):
224 bottom
= self
._get
_field
('bottom.old')
225 top
= self
._get
_field
('top.old')
228 self
._set
_field
('bottom', bottom
)
229 self
._set
_field
('top', top
)
230 self
.__update
_top
_ref
(top
)
235 def get_description(self
):
236 return self
._get
_field
('description', True)
238 def set_description(self
, line
):
239 self
._set
_field
('description', line
, True)
241 def get_authname(self
):
242 return self
._get
_field
('authname')
244 def set_authname(self
, name
):
245 self
._set
_field
('authname', name
or git
.author().name
)
247 def get_authemail(self
):
248 return self
._get
_field
('authemail')
250 def set_authemail(self
, email
):
251 self
._set
_field
('authemail', email
or git
.author().email
)
253 def get_authdate(self
):
254 return self
._get
_field
('authdate')
256 def set_authdate(self
, date
):
257 self
._set
_field
('authdate', date
or git
.author().date
)
259 def get_commname(self
):
260 return self
._get
_field
('commname')
262 def set_commname(self
, name
):
263 self
._set
_field
('commname', name
or git
.committer().name
)
265 def get_commemail(self
):
266 return self
._get
_field
('commemail')
268 def set_commemail(self
, email
):
269 self
._set
_field
('commemail', email
or git
.committer().email
)
272 return self
._get
_field
('log')
274 def set_log(self
, value
, backup
= False):
275 self
._set
_field
('log', value
)
276 self
.__update
_log
_ref
(value
)
278 # The current StGIT metadata format version.
281 class PatchSet(StgitObject
):
282 def __init__(self
, name
= None):
287 self
.set_name (git
.get_head_file())
288 self
.__base
_dir
= basedir
.get()
289 except git
.GitException
, ex
:
290 raise StackException
, 'GIT tree not initialised: %s' % ex
292 self
._set
_dir
(os
.path
.join(self
.__base
_dir
, 'patches', self
.get_name()))
296 def set_name(self
, name
):
300 return self
.__base
_dir
303 """Return the head of the branch
305 crt
= self
.get_current_patch()
309 return self
.get_base()
311 def get_protected(self
):
312 return os
.path
.isfile(os
.path
.join(self
._dir
(), 'protected'))
315 protect_file
= os
.path
.join(self
._dir
(), 'protected')
316 if not os
.path
.isfile(protect_file
):
317 create_empty_file(protect_file
)
320 protect_file
= os
.path
.join(self
._dir
(), 'protected')
321 if os
.path
.isfile(protect_file
):
322 os
.remove(protect_file
)
324 def __branch_descr(self
):
325 return 'branch.%s.description' % self
.get_name()
327 def get_description(self
):
328 return config
.get(self
.__branch
_descr
()) or ''
330 def set_description(self
, line
):
332 config
.set(self
.__branch
_descr
(), line
)
334 config
.unset(self
.__branch
_descr
())
336 def head_top_equal(self
):
337 """Return true if the head and the top are the same
339 crt
= self
.get_current_patch()
341 # we don't care, no patches applied
343 return git
.get_head() == crt
.get_top()
345 def is_initialised(self
):
346 """Checks if series is already initialised
348 return bool(config
.get(self
.format_version_key()))
351 class Series(PatchSet
):
352 """Class including the operations on series
354 def __init__(self
, name
= None):
355 """Takes a series name as the parameter.
357 PatchSet
.__init
__(self
, name
)
359 # Update the branch to the latest format version if it is
360 # initialized, but don't touch it if it isn't.
361 self
.update_to_current_format_version()
363 self
.__refs
_base
= 'refs/patches/%s' % self
.get_name()
365 self
.__applied
_file
= os
.path
.join(self
._dir
(), 'applied')
366 self
.__unapplied
_file
= os
.path
.join(self
._dir
(), 'unapplied')
367 self
.__hidden
_file
= os
.path
.join(self
._dir
(), 'hidden')
369 # where this series keeps its patches
370 self
.__patch
_dir
= os
.path
.join(self
._dir
(), 'patches')
373 self
.__trash
_dir
= os
.path
.join(self
._dir
(), 'trash')
375 def format_version_key(self
):
376 return 'branch.%s.stgit.stackformatversion' % self
.get_name()
378 def update_to_current_format_version(self
):
379 """Update a potentially older StGIT directory structure to the
380 latest version. Note: This function should depend as little as
381 possible on external functions that may change during a format
382 version bump, since it must remain able to process older formats."""
384 branch_dir
= os
.path
.join(self
._basedir
(), 'patches', self
.get_name())
385 def get_format_version():
386 """Return the integer format version number, or None if the
387 branch doesn't have any StGIT metadata at all, of any version."""
388 fv
= config
.get(self
.format_version_key())
389 ofv
= config
.get('branch.%s.stgitformatversion' % self
.get_name())
391 # Great, there's an explicitly recorded format version
392 # number, which means that the branch is initialized and
393 # of that exact version.
396 # Old name for the version info, upgrade it
397 config
.set(self
.format_version_key(), ofv
)
398 config
.unset('branch.%s.stgitformatversion' % self
.get_name())
400 elif os
.path
.isdir(os
.path
.join(branch_dir
, 'patches')):
401 # There's a .git/patches/<branch>/patches dirctory, which
402 # means this is an initialized version 1 branch.
404 elif os
.path
.isdir(branch_dir
):
405 # There's a .git/patches/<branch> directory, which means
406 # this is an initialized version 0 branch.
409 # The branch doesn't seem to be initialized at all.
411 def set_format_version(v
):
412 out
.info('Upgraded branch %s to format version %d' % (self
.get_name(), v
))
413 config
.set(self
.format_version_key(), '%d' % v
)
415 if not os
.path
.isdir(d
):
418 if os
.path
.exists(f
):
421 if git
.ref_exists(ref
):
425 if get_format_version() == 0:
426 mkdir(os
.path
.join(branch_dir
, 'trash'))
427 patch_dir
= os
.path
.join(branch_dir
, 'patches')
429 refs_base
= 'refs/patches/%s' % self
.get_name()
430 for patch
in (file(os
.path
.join(branch_dir
, 'unapplied')).readlines()
431 + file(os
.path
.join(branch_dir
, 'applied')).readlines()):
432 patch
= patch
.strip()
433 os
.rename(os
.path
.join(branch_dir
, patch
),
434 os
.path
.join(patch_dir
, patch
))
435 Patch(patch
, patch_dir
, refs_base
).update_top_ref()
436 set_format_version(1)
439 if get_format_version() == 1:
440 desc_file
= os
.path
.join(branch_dir
, 'description')
441 if os
.path
.isfile(desc_file
):
442 desc
= read_string(desc_file
)
444 config
.set('branch.%s.description' % self
.get_name(), desc
)
446 rm(os
.path
.join(branch_dir
, 'current'))
447 rm_ref('refs/bases/%s' % self
.get_name())
448 set_format_version(2)
450 # Make sure we're at the latest version.
451 if not get_format_version() in [None, FORMAT_VERSION
]:
452 raise StackException('Branch %s is at format version %d, expected %d'
453 % (self
.get_name(), get_format_version(), FORMAT_VERSION
))
455 def __patch_name_valid(self
, name
):
456 """Raise an exception if the patch name is not valid.
458 if not name
or re
.search('[^\w.-]', name
):
459 raise StackException
, 'Invalid patch name: "%s"' % name
461 def get_patch(self
, name
):
462 """Return a Patch object for the given name
464 return Patch(name
, self
.__patch
_dir
, self
.__refs
_base
)
466 def get_current_patch(self
):
467 """Return a Patch object representing the topmost patch, or
468 None if there is no such patch."""
469 crt
= self
.get_current()
472 return self
.get_patch(crt
)
474 def get_current(self
):
475 """Return the name of the topmost patch, or None if there is
478 applied
= self
.get_applied()
479 except StackException
:
480 # No "applied" file: branch is not initialized.
485 # No patches applied.
488 def get_applied(self
):
489 if not os
.path
.isfile(self
.__applied
_file
):
490 raise StackException
, 'Branch "%s" not initialised' % self
.get_name()
491 return read_strings(self
.__applied
_file
)
493 def get_unapplied(self
):
494 if not os
.path
.isfile(self
.__unapplied
_file
):
495 raise StackException
, 'Branch "%s" not initialised' % self
.get_name()
496 return read_strings(self
.__unapplied
_file
)
498 def get_hidden(self
):
499 if not os
.path
.isfile(self
.__hidden
_file
):
501 return read_strings(self
.__hidden
_file
)
504 # Return the parent of the bottommost patch, if there is one.
505 if os
.path
.isfile(self
.__applied
_file
):
506 bottommost
= file(self
.__applied
_file
).readline().strip()
508 return self
.get_patch(bottommost
).get_bottom()
509 # No bottommost patch, so just return HEAD
510 return git
.get_head()
512 def get_parent_remote(self
):
513 value
= config
.get('branch.%s.remote' % self
.get_name())
516 elif 'origin' in git
.remotes_list():
517 out
.note(('No parent remote declared for stack "%s",'
518 ' defaulting to "origin".' % self
.get_name()),
519 ('Consider setting "branch.%s.remote" and'
520 ' "branch.%s.merge" with "git config".'
521 % (self
.get_name(), self
.get_name())))
524 raise StackException
, 'Cannot find a parent remote for "%s"' % self
.get_name()
526 def __set_parent_remote(self
, remote
):
527 value
= config
.set('branch.%s.remote' % self
.get_name(), remote
)
529 def get_parent_branch(self
):
530 value
= config
.get('branch.%s.stgit.parentbranch' % self
.get_name())
533 elif git
.rev_parse('heads/origin'):
534 out
.note(('No parent branch declared for stack "%s",'
535 ' defaulting to "heads/origin".' % self
.get_name()),
536 ('Consider setting "branch.%s.stgit.parentbranch"'
537 ' with "git config".' % self
.get_name()))
538 return 'heads/origin'
540 raise StackException
, 'Cannot find a parent branch for "%s"' % self
.get_name()
542 def __set_parent_branch(self
, name
):
543 if config
.get('branch.%s.remote' % self
.get_name()):
544 # Never set merge if remote is not set to avoid
545 # possibly-erroneous lookups into 'origin'
546 config
.set('branch.%s.merge' % self
.get_name(), name
)
547 config
.set('branch.%s.stgit.parentbranch' % self
.get_name(), name
)
549 def set_parent(self
, remote
, localbranch
):
552 self
.__set
_parent
_remote
(remote
)
553 self
.__set
_parent
_branch
(localbranch
)
554 # We'll enforce this later
556 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.get_name()
558 def __patch_is_current(self
, patch
):
559 return patch
.get_name() == self
.get_current()
561 def patch_applied(self
, name
):
562 """Return true if the patch exists in the applied list
564 return name
in self
.get_applied()
566 def patch_unapplied(self
, name
):
567 """Return true if the patch exists in the unapplied list
569 return name
in self
.get_unapplied()
571 def patch_hidden(self
, name
):
572 """Return true if the patch is hidden.
574 return name
in self
.get_hidden()
576 def patch_exists(self
, name
):
577 """Return true if there is a patch with the given name, false
579 return self
.patch_applied(name
) or self
.patch_unapplied(name
) \
580 or self
.patch_hidden(name
)
582 def init(self
, create_at
=False, parent_remote
=None, parent_branch
=None):
583 """Initialises the stgit series
585 if self
.is_initialised():
586 raise StackException
, '%s already initialized' % self
.get_name()
587 for d
in [self
._dir
()]:
588 if os
.path
.exists(d
):
589 raise StackException
, '%s already exists' % d
591 if (create_at
!=False):
592 git
.create_branch(self
.get_name(), create_at
)
594 os
.makedirs(self
.__patch
_dir
)
596 self
.set_parent(parent_remote
, parent_branch
)
598 self
.create_empty_field('applied')
599 self
.create_empty_field('unapplied')
600 self
._set
_field
('orig-base', git
.get_head())
602 config
.set(self
.format_version_key(), str(FORMAT_VERSION
))
604 def rename(self
, to_name
):
607 to_stack
= Series(to_name
)
609 if to_stack
.is_initialised():
610 raise StackException
, '"%s" already exists' % to_stack
.get_name()
612 patches
= self
.get_applied() + self
.get_unapplied()
614 git
.rename_branch(self
.get_name(), to_name
)
616 for patch
in patches
:
617 git
.rename_ref('refs/patches/%s/%s' % (self
.get_name(), patch
),
618 'refs/patches/%s/%s' % (to_name
, patch
))
619 git
.rename_ref('refs/patches/%s/%s.log' % (self
.get_name(), patch
),
620 'refs/patches/%s/%s.log' % (to_name
, patch
))
621 if os
.path
.isdir(self
._dir
()):
622 rename(os
.path
.join(self
._basedir
(), 'patches'),
623 self
.get_name(), to_stack
.get_name())
625 # Rename the config section
626 for k
in ['branch.%s', 'branch.%s.stgit']:
627 config
.rename_section(k
% self
.get_name(), k
% to_name
)
629 self
.__init
__(to_name
)
631 def clone(self
, target_series
):
635 # allow cloning of branches not under StGIT control
636 base
= self
.get_base()
638 base
= git
.get_head()
639 Series(target_series
).init(create_at
= base
)
640 new_series
= Series(target_series
)
642 # generate an artificial description file
643 new_series
.set_description('clone of "%s"' % self
.get_name())
645 # clone self's entire series as unapplied patches
647 # allow cloning of branches not under StGIT control
648 applied
= self
.get_applied()
649 unapplied
= self
.get_unapplied()
650 patches
= applied
+ unapplied
653 patches
= applied
= unapplied
= []
655 patch
= self
.get_patch(p
)
656 newpatch
= new_series
.new_patch(p
, message
= patch
.get_description(),
657 can_edit
= False, unapplied
= True,
658 bottom
= patch
.get_bottom(),
659 top
= patch
.get_top(),
660 author_name
= patch
.get_authname(),
661 author_email
= patch
.get_authemail(),
662 author_date
= patch
.get_authdate())
664 out
.info('Setting log to %s' % patch
.get_log())
665 newpatch
.set_log(patch
.get_log())
667 out
.info('No log for %s' % p
)
669 # fast forward the cloned series to self's top
670 new_series
.forward_patches(applied
)
672 # Clone parent informations
673 value
= config
.get('branch.%s.remote' % self
.get_name())
675 config
.set('branch.%s.remote' % target_series
, value
)
677 value
= config
.get('branch.%s.merge' % self
.get_name())
679 config
.set('branch.%s.merge' % target_series
, value
)
681 value
= config
.get('branch.%s.stgit.parentbranch' % self
.get_name())
683 config
.set('branch.%s.stgit.parentbranch' % target_series
, value
)
685 def delete(self
, force
= False):
686 """Deletes an stgit series
688 if self
.is_initialised():
689 patches
= self
.get_unapplied() + self
.get_applied()
690 if not force
and patches
:
691 raise StackException
, \
692 'Cannot delete: the series still contains patches'
694 self
.get_patch(p
).delete()
696 # remove the trash directory if any
697 if os
.path
.exists(self
.__trash
_dir
):
698 for fname
in os
.listdir(self
.__trash
_dir
):
699 os
.remove(os
.path
.join(self
.__trash
_dir
, fname
))
700 os
.rmdir(self
.__trash
_dir
)
702 # FIXME: find a way to get rid of those manual removals
703 # (move functionality to StgitObject ?)
704 if os
.path
.exists(self
.__applied
_file
):
705 os
.remove(self
.__applied
_file
)
706 if os
.path
.exists(self
.__unapplied
_file
):
707 os
.remove(self
.__unapplied
_file
)
708 if os
.path
.exists(self
.__hidden
_file
):
709 os
.remove(self
.__hidden
_file
)
710 if os
.path
.exists(self
._dir
()+'/orig-base'):
711 os
.remove(self
._dir
()+'/orig-base')
713 if not os
.listdir(self
.__patch
_dir
):
714 os
.rmdir(self
.__patch
_dir
)
716 out
.warn('Patch directory %s is not empty' % self
.__patch
_dir
)
719 os
.removedirs(self
._dir
())
721 raise StackException('Series directory %s is not empty'
725 git
.delete_branch(self
.get_name())
727 out
.warn('Could not delete branch "%s"' % self
.get_name())
729 # Cleanup parent informations
730 # FIXME: should one day make use of git-config --section-remove,
731 # scheduled for 1.5.1
732 config
.unset('branch.%s.remote' % self
.get_name())
733 config
.unset('branch.%s.merge' % self
.get_name())
734 config
.unset('branch.%s.stgit.parentbranch' % self
.get_name())
735 config
.unset(self
.format_version_key())
737 def refresh_patch(self
, files
= None, message
= None, edit
= False,
740 author_name
= None, author_email
= None,
742 committer_name
= None, committer_email
= None,
743 backup
= False, sign_str
= None, log
= 'refresh',
745 """Generates a new commit for the given patch
747 name
= self
.get_current()
749 raise StackException
, 'No patches applied'
751 patch
= self
.get_patch(name
)
753 descr
= patch
.get_description()
754 if not (message
or descr
):
760 if not message
and edit
:
761 descr
= edit_file(self
, descr
.rstrip(), \
762 'Please edit the description for patch "%s" ' \
763 'above.' % name
, show_patch
)
766 author_name
= patch
.get_authname()
768 author_email
= patch
.get_authemail()
770 author_date
= patch
.get_authdate()
771 if not committer_name
:
772 committer_name
= patch
.get_commname()
773 if not committer_email
:
774 committer_email
= patch
.get_commemail()
776 descr
= add_sign_line(descr
, sign_str
, committer_name
, committer_email
)
778 bottom
= patch
.get_bottom()
780 commit_id
= git
.commit(files
= files
,
781 message
= descr
, parents
= [bottom
],
782 cache_update
= cache_update
,
784 author_name
= author_name
,
785 author_email
= author_email
,
786 author_date
= author_date
,
787 committer_name
= committer_name
,
788 committer_email
= committer_email
)
790 patch
.set_bottom(bottom
, backup
= backup
)
791 patch
.set_top(commit_id
, backup
= backup
)
792 patch
.set_description(descr
)
793 patch
.set_authname(author_name
)
794 patch
.set_authemail(author_email
)
795 patch
.set_authdate(author_date
)
796 patch
.set_commname(committer_name
)
797 patch
.set_commemail(committer_email
)
800 self
.log_patch(patch
, log
, notes
)
804 def undo_refresh(self
):
805 """Undo the patch boundaries changes caused by 'refresh'
807 name
= self
.get_current()
810 patch
= self
.get_patch(name
)
811 old_bottom
= patch
.get_old_bottom()
812 old_top
= patch
.get_old_top()
814 # the bottom of the patch is not changed by refresh. If the
815 # old_bottom is different, there wasn't any previous 'refresh'
816 # command (probably only a 'push')
817 if old_bottom
!= patch
.get_bottom() or old_top
== patch
.get_top():
818 raise StackException
, 'No undo information available'
820 git
.reset(tree_id
= old_top
, check_out
= False)
821 if patch
.restore_old_boundaries():
822 self
.log_patch(patch
, 'undo')
824 def new_patch(self
, name
, message
= None, can_edit
= True,
825 unapplied
= False, show_patch
= False,
826 top
= None, bottom
= None, commit
= True,
827 author_name
= None, author_email
= None, author_date
= None,
828 committer_name
= None, committer_email
= None,
829 before_existing
= False):
830 """Creates a new patch
834 self
.__patch
_name
_valid
(name
)
835 if self
.patch_exists(name
):
836 raise StackException
, 'Patch "%s" already exists' % name
838 if not message
and can_edit
:
841 'Please enter the description for the patch above.',
846 head
= git
.get_head()
849 name
= make_patch_name(descr
, self
.patch_exists
)
851 patch
= self
.get_patch(name
)
859 patch
.set_bottom(bottom
)
861 patch
.set_description(descr
)
862 patch
.set_authname(author_name
)
863 patch
.set_authemail(author_email
)
864 patch
.set_authdate(author_date
)
865 patch
.set_commname(committer_name
)
866 patch
.set_commemail(committer_email
)
869 insert_string(self
.__applied
_file
, patch
.get_name())
870 # no need to commit anything as the object is already
871 # present (mainly used by 'uncommit')
874 patches
= [patch
.get_name()] + self
.get_unapplied()
875 write_strings(self
.__unapplied
_file
, patches
)
878 append_string(self
.__applied
_file
, patch
.get_name())
882 # create a commit for the patch (may be empty if top == bottom);
883 # only commit on top of the current branch
884 assert(unapplied
or bottom
== head
)
885 top_commit
= git
.get_commit(top
)
886 commit_id
= git
.commit(message
= descr
, parents
= [bottom
],
887 cache_update
= False,
888 tree_id
= top_commit
.get_tree(),
889 allowempty
= True, set_head
= set_head
,
890 author_name
= author_name
,
891 author_email
= author_email
,
892 author_date
= author_date
,
893 committer_name
= committer_name
,
894 committer_email
= committer_email
)
895 # set the patch top to the new commit
896 patch
.set_top(commit_id
)
898 self
.log_patch(patch
, 'new')
902 def delete_patch(self
, name
):
905 self
.__patch
_name
_valid
(name
)
906 patch
= self
.get_patch(name
)
908 if self
.__patch
_is
_current
(patch
):
910 elif self
.patch_applied(name
):
911 raise StackException
, 'Cannot remove an applied patch, "%s", ' \
912 'which is not current' % name
913 elif not name
in self
.get_unapplied():
914 raise StackException
, 'Unknown patch "%s"' % name
916 # save the commit id to a trash file
917 write_string(os
.path
.join(self
.__trash
_dir
, name
), patch
.get_top())
921 unapplied
= self
.get_unapplied()
922 unapplied
.remove(name
)
923 write_strings(self
.__unapplied
_file
, unapplied
)
925 def forward_patches(self
, names
):
926 """Try to fast-forward an array of patches.
928 On return, patches in names[0:returned_value] have been pushed on the
929 stack. Apply the rest with push_patch
931 unapplied
= self
.get_unapplied()
937 assert(name
in unapplied
)
939 patch
= self
.get_patch(name
)
942 bottom
= patch
.get_bottom()
943 top
= patch
.get_top()
945 # top != bottom always since we have a commit for each patch
947 # reset the backup information. No logging since the
948 # patch hasn't changed
949 patch
.set_bottom(head
, backup
= True)
950 patch
.set_top(top
, backup
= True)
953 head_tree
= git
.get_commit(head
).get_tree()
954 bottom_tree
= git
.get_commit(bottom
).get_tree()
955 if head_tree
== bottom_tree
:
956 # We must just reparent this patch and create a new commit
958 descr
= patch
.get_description()
959 author_name
= patch
.get_authname()
960 author_email
= patch
.get_authemail()
961 author_date
= patch
.get_authdate()
962 committer_name
= patch
.get_commname()
963 committer_email
= patch
.get_commemail()
965 top_tree
= git
.get_commit(top
).get_tree()
967 top
= git
.commit(message
= descr
, parents
= [head
],
968 cache_update
= False,
971 author_name
= author_name
,
972 author_email
= author_email
,
973 author_date
= author_date
,
974 committer_name
= committer_name
,
975 committer_email
= committer_email
)
977 patch
.set_bottom(head
, backup
= True)
978 patch
.set_top(top
, backup
= True)
980 self
.log_patch(patch
, 'push(f)')
983 # stop the fast-forwarding, must do a real merge
987 unapplied
.remove(name
)
994 append_strings(self
.__applied
_file
, names
[0:forwarded
])
995 write_strings(self
.__unapplied
_file
, unapplied
)
999 def merged_patches(self
, names
):
1000 """Test which patches were merged upstream by reverse-applying
1001 them in reverse order. The function returns the list of
1002 patches detected to have been applied. The state of the tree
1003 is restored to the original one
1005 patches
= [self
.get_patch(name
) for name
in names
]
1010 if git
.apply_diff(p
.get_top(), p
.get_bottom()):
1011 merged
.append(p
.get_name())
1018 def push_patch(self
, name
, empty
= False):
1019 """Pushes a patch on the stack
1021 unapplied
= self
.get_unapplied()
1022 assert(name
in unapplied
)
1024 patch
= self
.get_patch(name
)
1026 head
= git
.get_head()
1027 bottom
= patch
.get_bottom()
1028 top
= patch
.get_top()
1033 # top != bottom always since we have a commit for each patch
1035 # just make an empty patch (top = bottom = HEAD). This
1036 # option is useful to allow undoing already merged
1037 # patches. The top is updated by refresh_patch since we
1038 # need an empty commit
1039 patch
.set_bottom(head
, backup
= True)
1040 patch
.set_top(head
, backup
= True)
1042 elif head
== bottom
:
1043 # reset the backup information. No need for logging
1044 patch
.set_bottom(bottom
, backup
= True)
1045 patch
.set_top(top
, backup
= True)
1049 # new patch needs to be refreshed.
1050 # The current patch is empty after merge.
1051 patch
.set_bottom(head
, backup
= True)
1052 patch
.set_top(head
, backup
= True)
1054 # Try the fast applying first. If this fails, fall back to the
1056 if not git
.apply_diff(bottom
, top
):
1057 # if git.apply_diff() fails, the patch requires a diff3
1058 # merge and can be reported as modified
1061 # merge can fail but the patch needs to be pushed
1063 git
.merge(bottom
, head
, top
, recursive
= True)
1064 except git
.GitException
, ex
:
1065 out
.error('The merge failed during "push".',
1066 'Use "refresh" after fixing the conflicts or'
1067 ' revert the operation with "push --undo".')
1069 append_string(self
.__applied
_file
, name
)
1071 unapplied
.remove(name
)
1072 write_strings(self
.__unapplied
_file
, unapplied
)
1074 # head == bottom case doesn't need to refresh the patch
1075 if empty
or head
!= bottom
:
1077 # if the merge was OK and no conflicts, just refresh the patch
1078 # The GIT cache was already updated by the merge operation
1083 self
.refresh_patch(cache_update
= False, log
= log
)
1085 # we store the correctly merged files only for
1086 # tracking the conflict history. Note that the
1087 # git.merge() operations should always leave the index
1088 # in a valid state (i.e. only stage 0 files)
1089 self
.refresh_patch(cache_update
= False, log
= 'push(c)')
1090 raise StackException
, str(ex
)
1094 def undo_push(self
):
1095 name
= self
.get_current()
1098 patch
= self
.get_patch(name
)
1099 old_bottom
= patch
.get_old_bottom()
1100 old_top
= patch
.get_old_top()
1102 # the top of the patch is changed by a push operation only
1103 # together with the bottom (otherwise the top was probably
1104 # modified by 'refresh'). If they are both unchanged, there
1105 # was a fast forward
1106 if old_bottom
== patch
.get_bottom() and old_top
!= patch
.get_top():
1107 raise StackException
, 'No undo information available'
1110 self
.pop_patch(name
)
1111 ret
= patch
.restore_old_boundaries()
1113 self
.log_patch(patch
, 'undo')
1117 def pop_patch(self
, name
, keep
= False):
1118 """Pops the top patch from the stack
1120 applied
= self
.get_applied()
1122 assert(name
in applied
)
1124 patch
= self
.get_patch(name
)
1126 if git
.get_head_file() == self
.get_name():
1127 if keep
and not git
.apply_diff(git
.get_head(), patch
.get_bottom()):
1128 raise StackException(
1129 'Failed to pop patches while preserving the local changes')
1130 git
.switch(patch
.get_bottom(), keep
)
1132 git
.set_branch(self
.get_name(), patch
.get_bottom())
1134 # save the new applied list
1135 idx
= applied
.index(name
) + 1
1137 popped
= applied
[:idx
]
1139 unapplied
= popped
+ self
.get_unapplied()
1140 write_strings(self
.__unapplied
_file
, unapplied
)
1144 write_strings(self
.__applied
_file
, applied
)
1146 def empty_patch(self
, name
):
1147 """Returns True if the patch is empty
1149 self
.__patch
_name
_valid
(name
)
1150 patch
= self
.get_patch(name
)
1151 bottom
= patch
.get_bottom()
1152 top
= patch
.get_top()
1156 elif git
.get_commit(top
).get_tree() \
1157 == git
.get_commit(bottom
).get_tree():
1162 def rename_patch(self
, oldname
, newname
):
1163 self
.__patch
_name
_valid
(newname
)
1165 applied
= self
.get_applied()
1166 unapplied
= self
.get_unapplied()
1168 if oldname
== newname
:
1169 raise StackException
, '"To" name and "from" name are the same'
1171 if newname
in applied
or newname
in unapplied
:
1172 raise StackException
, 'Patch "%s" already exists' % newname
1174 if oldname
in unapplied
:
1175 self
.get_patch(oldname
).rename(newname
)
1176 unapplied
[unapplied
.index(oldname
)] = newname
1177 write_strings(self
.__unapplied
_file
, unapplied
)
1178 elif oldname
in applied
:
1179 self
.get_patch(oldname
).rename(newname
)
1181 applied
[applied
.index(oldname
)] = newname
1182 write_strings(self
.__applied
_file
, applied
)
1184 raise StackException
, 'Unknown patch "%s"' % oldname
1186 def log_patch(self
, patch
, message
, notes
= None):
1187 """Generate a log commit for a patch
1189 top
= git
.get_commit(patch
.get_top())
1190 old_log
= patch
.get_log()
1193 # replace the current log entry
1195 raise StackException
, \
1196 'No log entry to annotate for patch "%s"' \
1199 log_commit
= git
.get_commit(old_log
)
1200 msg
= log_commit
.get_log().split('\n')[0]
1201 log_parent
= log_commit
.get_parent()
1203 parents
= [log_parent
]
1207 # generate a new log entry
1209 msg
= '%s\t%s' % (message
, top
.get_id_hash())
1216 msg
+= '\n\n' + notes
1218 log
= git
.commit(message
= msg
, parents
= parents
,
1219 cache_update
= False, tree_id
= top
.get_tree(),
1223 def hide_patch(self
, name
):
1224 """Add the patch to the hidden list.
1226 unapplied
= self
.get_unapplied()
1227 if name
not in unapplied
:
1228 # keep the checking order for backward compatibility with
1229 # the old hidden patches functionality
1230 if self
.patch_applied(name
):
1231 raise StackException
, 'Cannot hide applied patch "%s"' % name
1232 elif self
.patch_hidden(name
):
1233 raise StackException
, 'Patch "%s" already hidden' % name
1235 raise StackException
, 'Unknown patch "%s"' % name
1237 if not self
.patch_hidden(name
):
1238 # check needed for backward compatibility with the old
1239 # hidden patches functionality
1240 append_string(self
.__hidden
_file
, name
)
1242 unapplied
.remove(name
)
1243 write_strings(self
.__unapplied
_file
, unapplied
)
1245 def unhide_patch(self
, name
):
1246 """Remove the patch from the hidden list.
1248 hidden
= self
.get_hidden()
1249 if not name
in hidden
:
1250 if self
.patch_applied(name
) or self
.patch_unapplied(name
):
1251 raise StackException
, 'Patch "%s" not hidden' % name
1253 raise StackException
, 'Unknown patch "%s"' % name
1256 write_strings(self
.__hidden
_file
, hidden
)
1258 if not self
.patch_applied(name
) and not self
.patch_unapplied(name
):
1259 # check needed for backward compatibility with the old
1260 # hidden patches functionality
1261 append_string(self
.__unapplied
_file
, name
)