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
26 from shutil
import copyfile
29 # stack exception class
30 class StackException(Exception):
35 self
.should_print
= True
36 def __call__(self
, x
, until_test
, prefix
):
38 self
.should_print
= False
40 return x
[0:len(prefix
)] != prefix
46 __comment_prefix
= 'STG:'
47 __patch_prefix
= 'STG_PATCH:'
49 def __clean_comments(f
):
50 """Removes lines marked for status in a commit file
54 # remove status-prefixed lines
57 patch_filter
= FilterUntil()
58 until_test
= lambda t
: t
== (__patch_prefix
+ '\n')
59 lines
= [l
for l
in lines
if patch_filter(l
, until_test
, __comment_prefix
)]
61 # remove empty lines at the end
62 while len(lines
) != 0 and lines
[-1] == '\n':
65 f
.seek(0); f
.truncate()
68 def edit_file(series
, line
, comment
, show_patch
= True):
69 fname
= '.stgitmsg.txt'
70 tmpl
= templates
.get_template('patchdescr.tmpl')
79 print >> f
, __comment_prefix
, comment
80 print >> f
, __comment_prefix
, \
81 'Lines prefixed with "%s" will be automatically removed.' \
83 print >> f
, __comment_prefix
, \
84 'Trailing empty lines will be automatically removed.'
87 print >> f
, __patch_prefix
88 # series.get_patch(series.get_current()).get_top()
89 git
.diff([], series
.get_patch(series
.get_current()).get_bottom(), None, f
)
91 #Vim modeline must be near the end.
92 print >> f
, __comment_prefix
, 'vi: set textwidth=75 filetype=diff nobackup:'
113 """An object with stgit-like properties stored as files in a directory
115 def _set_dir(self
, dir):
120 def create_empty_field(self
, name
):
121 create_empty_file(os
.path
.join(self
.__dir
, name
))
123 def _get_field(self
, name
, multiline
= False):
124 id_file
= os
.path
.join(self
.__dir
, name
)
125 if os
.path
.isfile(id_file
):
126 line
= read_string(id_file
, multiline
)
134 def _set_field(self
, name
, value
, multiline
= False):
135 fname
= os
.path
.join(self
.__dir
, name
)
136 if value
and value
!= '':
137 write_string(fname
, value
, multiline
)
138 elif os
.path
.isfile(fname
):
142 class Patch(StgitObject
):
143 """Basic patch implementation
145 def __init__(self
, name
, series_dir
, refs_dir
):
146 self
.__series
_dir
= series_dir
148 self
._set
_dir
(os
.path
.join(self
.__series
_dir
, self
.__name
))
149 self
.__refs
_dir
= refs_dir
150 self
.__top
_ref
_file
= os
.path
.join(self
.__refs
_dir
, self
.__name
)
151 self
.__log
_ref
_file
= os
.path
.join(self
.__refs
_dir
,
152 self
.__name
+ '.log')
155 os
.mkdir(self
._dir
())
156 self
.create_empty_field('bottom')
157 self
.create_empty_field('top')
160 for f
in os
.listdir(self
._dir
()):
161 os
.remove(os
.path
.join(self
._dir
(), f
))
162 os
.rmdir(self
._dir
())
163 os
.remove(self
.__top
_ref
_file
)
164 if os
.path
.exists(self
.__log
_ref
_file
):
165 os
.remove(self
.__log
_ref
_file
)
170 def rename(self
, newname
):
172 old_top_ref_file
= self
.__top
_ref
_file
173 old_log_ref_file
= self
.__log
_ref
_file
174 self
.__name
= newname
175 self
._set
_dir
(os
.path
.join(self
.__series
_dir
, self
.__name
))
176 self
.__top
_ref
_file
= os
.path
.join(self
.__refs
_dir
, self
.__name
)
177 self
.__log
_ref
_file
= os
.path
.join(self
.__refs
_dir
,
178 self
.__name
+ '.log')
180 os
.rename(olddir
, self
._dir
())
181 os
.rename(old_top_ref_file
, self
.__top
_ref
_file
)
182 if os
.path
.exists(old_log_ref_file
):
183 os
.rename(old_log_ref_file
, self
.__log
_ref
_file
)
185 def __update_top_ref(self
, ref
):
186 write_string(self
.__top
_ref
_file
, ref
)
188 def __update_log_ref(self
, ref
):
189 write_string(self
.__log
_ref
_file
, ref
)
191 def update_top_ref(self
):
194 self
.__update
_top
_ref
(top
)
196 def get_old_bottom(self
):
197 return self
._get
_field
('bottom.old')
199 def get_bottom(self
):
200 return self
._get
_field
('bottom')
202 def set_bottom(self
, value
, backup
= False):
204 curr
= self
._get
_field
('bottom')
205 self
._set
_field
('bottom.old', curr
)
206 self
._set
_field
('bottom', value
)
208 def get_old_top(self
):
209 return self
._get
_field
('top.old')
212 return self
._get
_field
('top')
214 def set_top(self
, value
, backup
= False):
216 curr
= self
._get
_field
('top')
217 self
._set
_field
('top.old', curr
)
218 self
._set
_field
('top', value
)
219 self
.__update
_top
_ref
(value
)
221 def restore_old_boundaries(self
):
222 bottom
= self
._get
_field
('bottom.old')
223 top
= self
._get
_field
('top.old')
226 self
._set
_field
('bottom', bottom
)
227 self
._set
_field
('top', top
)
228 self
.__update
_top
_ref
(top
)
233 def get_description(self
):
234 return self
._get
_field
('description', True)
236 def set_description(self
, line
):
237 self
._set
_field
('description', line
, True)
239 def get_authname(self
):
240 return self
._get
_field
('authname')
242 def set_authname(self
, name
):
243 self
._set
_field
('authname', name
or git
.author().name
)
245 def get_authemail(self
):
246 return self
._get
_field
('authemail')
248 def set_authemail(self
, email
):
249 self
._set
_field
('authemail', email
or git
.author().email
)
251 def get_authdate(self
):
252 return self
._get
_field
('authdate')
254 def set_authdate(self
, date
):
255 self
._set
_field
('authdate', date
or git
.author().date
)
257 def get_commname(self
):
258 return self
._get
_field
('commname')
260 def set_commname(self
, name
):
261 self
._set
_field
('commname', name
or git
.committer().name
)
263 def get_commemail(self
):
264 return self
._get
_field
('commemail')
266 def set_commemail(self
, email
):
267 self
._set
_field
('commemail', email
or git
.committer().email
)
270 return self
._get
_field
('log')
272 def set_log(self
, value
, backup
= False):
273 self
._set
_field
('log', value
)
274 self
.__update
_log
_ref
(value
)
277 class Series(StgitObject
):
278 """Class including the operations on series
280 def __init__(self
, name
= None):
281 """Takes a series name as the parameter.
287 self
.__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
.__name
))
293 self
.__refs
_dir
= os
.path
.join(self
.__base
_dir
, 'refs', 'patches',
296 self
.__applied
_file
= os
.path
.join(self
._dir
(), 'applied')
297 self
.__unapplied
_file
= os
.path
.join(self
._dir
(), 'unapplied')
298 self
.__hidden
_file
= os
.path
.join(self
._dir
(), 'hidden')
300 # where this series keeps its patches
301 self
.__patch
_dir
= os
.path
.join(self
._dir
(), 'patches')
302 if not os
.path
.isdir(self
.__patch
_dir
):
303 self
.__patch
_dir
= self
._dir
()
305 # if no __refs_dir, create and populate it (upgrade old repositories)
306 if self
.is_initialised() and not os
.path
.isdir(self
.__refs
_dir
):
307 os
.makedirs(self
.__refs
_dir
)
308 for patch
in self
.get_applied() + self
.get_unapplied():
309 self
.get_patch(patch
).update_top_ref()
312 self
.__trash
_dir
= os
.path
.join(self
._dir
(), 'trash')
313 if self
.is_initialised() and not os
.path
.isdir(self
.__trash
_dir
):
314 os
.makedirs(self
.__trash
_dir
)
316 def __patch_name_valid(self
, name
):
317 """Raise an exception if the patch name is not valid.
319 if not name
or re
.search('[^\w.-]', name
):
320 raise StackException
, 'Invalid patch name: "%s"' % name
322 def get_branch(self
):
323 """Return the branch name for the Series object
327 def get_patch(self
, name
):
328 """Return a Patch object for the given name
330 return Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
332 def get_current_patch(self
):
333 """Return a Patch object representing the topmost patch, or
334 None if there is no such patch."""
335 crt
= self
.get_current()
338 return Patch(crt
, self
.__patch
_dir
, self
.__refs
_dir
)
340 def get_current(self
):
341 """Return the name of the topmost patch, or None if there is
344 applied
= self
.get_applied()
345 except StackException
:
346 # No "applied" file: branch is not initialized.
351 # No patches applied.
354 def get_applied(self
):
355 if not os
.path
.isfile(self
.__applied
_file
):
356 raise StackException
, 'Branch "%s" not initialised' % self
.__name
357 f
= file(self
.__applied
_file
)
358 names
= [line
.strip() for line
in f
.readlines()]
362 def get_unapplied(self
):
363 if not os
.path
.isfile(self
.__unapplied
_file
):
364 raise StackException
, 'Branch "%s" not initialised' % self
.__name
365 f
= file(self
.__unapplied
_file
)
366 names
= [line
.strip() for line
in f
.readlines()]
370 def get_hidden(self
):
371 if not os
.path
.isfile(self
.__hidden
_file
):
373 f
= file(self
.__hidden
_file
)
374 names
= [line
.strip() for line
in f
.readlines()]
379 # Return the parent of the bottommost patch, if there is one.
380 if os
.path
.isfile(self
.__applied
_file
):
381 bottommost
= file(self
.__applied
_file
).readline().strip()
383 return self
.get_patch(bottommost
).get_bottom()
384 # No bottommost patch, so just return HEAD
385 return git
.get_head()
388 """Return the head of the branch
390 crt
= self
.get_current_patch()
394 return self
.get_base()
396 def get_protected(self
):
397 return os
.path
.isfile(os
.path
.join(self
._dir
(), 'protected'))
400 protect_file
= os
.path
.join(self
._dir
(), 'protected')
401 if not os
.path
.isfile(protect_file
):
402 create_empty_file(protect_file
)
405 protect_file
= os
.path
.join(self
._dir
(), 'protected')
406 if os
.path
.isfile(protect_file
):
407 os
.remove(protect_file
)
409 def __branch_descr(self
):
410 return 'branch.%s.description' % self
.get_branch()
412 def get_description(self
):
413 # Fall back to the .git/patches/<branch>/description file if
414 # the config variable is unset.
415 return (config
.get(self
.__branch
_descr
())
416 or self
._get
_field
('description') or '')
418 def set_description(self
, line
):
420 config
.set(self
.__branch
_descr
(), line
)
422 config
.unset(self
.__branch
_descr
())
423 # Delete the old .git/patches/<branch>/description file if it
425 self
._set
_field
('description', None)
427 def get_parent_remote(self
):
428 value
= config
.get('branch.%s.remote' % self
.__name
)
431 elif 'origin' in git
.remotes_list():
432 print 'Notice: no parent remote declared for stack "%s", ' \
433 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
434 'and "branch.%s.merge" with "git repo-config".' \
435 % (self
.__name
, self
.__name
, self
.__name
)
438 raise StackException
, 'Cannot find a parent remote for "%s"' % self
.__name
440 def __set_parent_remote(self
, remote
):
441 value
= config
.set('branch.%s.remote' % self
.__name
, remote
)
443 def get_parent_branch(self
):
444 value
= config
.get('branch.%s.stgit.parentbranch' % self
.__name
)
447 elif git
.rev_parse('heads/origin'):
448 print 'Notice: no parent branch declared for stack "%s", ' \
449 'defaulting to "heads/origin". Consider setting ' \
450 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
451 % (self
.__name
, self
.__name
)
452 return 'heads/origin'
454 raise StackException
, 'Cannot find a parent branch for "%s"' % self
.__name
456 def __set_parent_branch(self
, name
):
457 if config
.get('branch.%s.remote' % self
.__name
):
458 # Never set merge if remote is not set to avoid
459 # possibly-erroneous lookups into 'origin'
460 config
.set('branch.%s.merge' % self
.__name
, name
)
461 config
.set('branch.%s.stgit.parentbranch' % self
.__name
, name
)
463 def set_parent(self
, remote
, localbranch
):
465 self
.__set
_parent
_remote
(remote
)
466 self
.__set
_parent
_branch
(localbranch
)
467 # We'll enforce this later
469 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
471 def __patch_is_current(self
, patch
):
472 return patch
.get_name() == self
.get_current()
474 def patch_applied(self
, name
):
475 """Return true if the patch exists in the applied list
477 return name
in self
.get_applied()
479 def patch_unapplied(self
, name
):
480 """Return true if the patch exists in the unapplied list
482 return name
in self
.get_unapplied()
484 def patch_hidden(self
, name
):
485 """Return true if the patch is hidden.
487 return name
in self
.get_hidden()
489 def patch_exists(self
, name
):
490 """Return true if there is a patch with the given name, false
492 return self
.patch_applied(name
) or self
.patch_unapplied(name
)
494 def head_top_equal(self
):
495 """Return true if the head and the top are the same
497 crt
= self
.get_current_patch()
499 # we don't care, no patches applied
501 return git
.get_head() == crt
.get_top()
503 def is_initialised(self
):
504 """Checks if series is already initialised
506 return os
.path
.isdir(self
.__patch
_dir
)
508 def init(self
, create_at
=False, parent_remote
=None, parent_branch
=None):
509 """Initialises the stgit series
511 if os
.path
.exists(self
.__patch
_dir
):
512 raise StackException
, self
.__patch
_dir
+ ' already exists'
513 if os
.path
.exists(self
.__refs
_dir
):
514 raise StackException
, self
.__refs
_dir
+ ' already exists'
516 if (create_at
!=False):
517 git
.create_branch(self
.__name
, create_at
)
519 os
.makedirs(self
.__patch
_dir
)
521 self
.set_parent(parent_remote
, parent_branch
)
523 self
.create_empty_field('applied')
524 self
.create_empty_field('unapplied')
525 os
.makedirs(os
.path
.join(self
._dir
(), 'patches'))
526 os
.makedirs(self
.__refs
_dir
)
527 self
._set
_field
('orig-base', git
.get_head())
530 """Either convert to use a separate patch directory, or
531 unconvert to place the patches in the same directory with
534 if self
.__patch
_dir
== self
._dir
():
535 print 'Converting old-style to new-style...',
538 self
.__patch
_dir
= os
.path
.join(self
._dir
(), 'patches')
539 os
.makedirs(self
.__patch
_dir
)
541 for p
in self
.get_applied() + self
.get_unapplied():
542 src
= os
.path
.join(self
._dir
(), p
)
543 dest
= os
.path
.join(self
.__patch
_dir
, p
)
549 print 'Converting new-style to old-style...',
552 for p
in self
.get_applied() + self
.get_unapplied():
553 src
= os
.path
.join(self
.__patch
_dir
, p
)
554 dest
= os
.path
.join(self
._dir
(), p
)
557 if not os
.listdir(self
.__patch
_dir
):
558 os
.rmdir(self
.__patch
_dir
)
561 print 'Patch directory %s is not empty.' % self
.__patch
_dir
563 self
.__patch
_dir
= self
._dir
()
565 def rename(self
, to_name
):
568 to_stack
= Series(to_name
)
570 if to_stack
.is_initialised():
571 raise StackException
, '"%s" already exists' % to_stack
.get_branch()
573 git
.rename_branch(self
.__name
, to_name
)
575 if os
.path
.isdir(self
._dir
()):
576 rename(os
.path
.join(self
.__base
_dir
, 'patches'),
577 self
.__name
, to_stack
.__name
)
578 if os
.path
.exists(self
.__refs
_dir
):
579 rename(os
.path
.join(self
.__base
_dir
, 'refs', 'patches'),
580 self
.__name
, to_stack
.__name
)
582 # Rename the config section
583 config
.rename_section("branch.%s" % self
.__name
,
584 "branch.%s" % to_name
)
586 self
.__init
__(to_name
)
588 def clone(self
, target_series
):
592 # allow cloning of branches not under StGIT control
593 base
= self
.get_base()
595 base
= git
.get_head()
596 Series(target_series
).init(create_at
= base
)
597 new_series
= Series(target_series
)
599 # generate an artificial description file
600 new_series
.set_description('clone of "%s"' % self
.__name
)
602 # clone self's entire series as unapplied patches
604 # allow cloning of branches not under StGIT control
605 applied
= self
.get_applied()
606 unapplied
= self
.get_unapplied()
607 patches
= applied
+ unapplied
610 patches
= applied
= unapplied
= []
612 patch
= self
.get_patch(p
)
613 newpatch
= new_series
.new_patch(p
, message
= patch
.get_description(),
614 can_edit
= False, unapplied
= True,
615 bottom
= patch
.get_bottom(),
616 top
= patch
.get_top(),
617 author_name
= patch
.get_authname(),
618 author_email
= patch
.get_authemail(),
619 author_date
= patch
.get_authdate())
621 print "setting log to %s" % patch
.get_log()
622 newpatch
.set_log(patch
.get_log())
624 print "no log for %s" % p
626 # fast forward the cloned series to self's top
627 new_series
.forward_patches(applied
)
629 # Clone parent informations
630 value
= config
.get('branch.%s.remote' % self
.__name
)
632 config
.set('branch.%s.remote' % target_series
, value
)
634 value
= config
.get('branch.%s.merge' % self
.__name
)
636 config
.set('branch.%s.merge' % target_series
, value
)
638 value
= config
.get('branch.%s.stgit.parentbranch' % self
.__name
)
640 config
.set('branch.%s.stgit.parentbranch' % target_series
, value
)
642 def delete(self
, force
= False):
643 """Deletes an stgit series
645 if self
.is_initialised():
646 patches
= self
.get_unapplied() + self
.get_applied()
647 if not force
and patches
:
648 raise StackException
, \
649 'Cannot delete: the series still contains patches'
651 Patch(p
, self
.__patch
_dir
, self
.__refs
_dir
).delete()
653 # remove the trash directory
654 for fname
in os
.listdir(self
.__trash
_dir
):
655 os
.remove(os
.path
.join(self
.__trash
_dir
, fname
))
656 os
.rmdir(self
.__trash
_dir
)
658 # FIXME: find a way to get rid of those manual removals
659 # (move functionality to StgitObject ?)
660 if os
.path
.exists(self
.__applied
_file
):
661 os
.remove(self
.__applied
_file
)
662 if os
.path
.exists(self
.__unapplied
_file
):
663 os
.remove(self
.__unapplied
_file
)
664 if os
.path
.exists(self
.__hidden
_file
):
665 os
.remove(self
.__hidden
_file
)
666 if os
.path
.exists(self
._dir
()+'/orig-base'):
667 os
.remove(self
._dir
()+'/orig-base')
669 # Remove obsolete files that StGIT no longer uses, but
670 # that might still be around if this is an old repository.
671 for obsolete
in ([os
.path
.join(self
._dir
(), fn
)
672 for fn
in ['current', 'description']]
673 + [os
.path
.join(self
.__base
_dir
,
674 'refs', 'bases', self
.__name
)]):
675 if os
.path
.exists(obsolete
):
678 if not os
.listdir(self
.__patch
_dir
):
679 os
.rmdir(self
.__patch
_dir
)
681 print 'Patch directory %s is not empty.' % self
.__patch
_dir
684 os
.removedirs(self
._dir
())
686 raise StackException
, 'Series directory %s is not empty.' % self
._dir
()
689 os
.removedirs(self
.__refs
_dir
)
691 print 'Refs directory %s is not empty.' % self
.__refs
_dir
693 # Cleanup parent informations
694 # FIXME: should one day make use of git-config --section-remove,
695 # scheduled for 1.5.1
696 config
.unset('branch.%s.remote' % self
.__name
)
697 config
.unset('branch.%s.merge' % self
.__name
)
698 config
.unset('branch.%s.stgit.parentbranch' % self
.__name
)
700 def refresh_patch(self
, files
= None, message
= None, edit
= False,
703 author_name
= None, author_email
= None,
705 committer_name
= None, committer_email
= None,
706 backup
= False, sign_str
= None, log
= 'refresh'):
707 """Generates a new commit for the given patch
709 name
= self
.get_current()
711 raise StackException
, 'No patches applied'
713 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
715 descr
= patch
.get_description()
716 if not (message
or descr
):
722 if not message
and edit
:
723 descr
= edit_file(self
, descr
.rstrip(), \
724 'Please edit the description for patch "%s" ' \
725 'above.' % name
, show_patch
)
728 author_name
= patch
.get_authname()
730 author_email
= patch
.get_authemail()
732 author_date
= patch
.get_authdate()
733 if not committer_name
:
734 committer_name
= patch
.get_commname()
735 if not committer_email
:
736 committer_email
= patch
.get_commemail()
739 descr
= descr
.rstrip()
740 if descr
.find("\nSigned-off-by:") < 0 \
741 and descr
.find("\nAcked-by:") < 0:
744 descr
= '%s\n%s: %s <%s>\n' % (descr
, sign_str
,
745 committer_name
, committer_email
)
747 bottom
= patch
.get_bottom()
749 commit_id
= git
.commit(files
= files
,
750 message
= descr
, parents
= [bottom
],
751 cache_update
= cache_update
,
753 author_name
= author_name
,
754 author_email
= author_email
,
755 author_date
= author_date
,
756 committer_name
= committer_name
,
757 committer_email
= committer_email
)
759 patch
.set_bottom(bottom
, backup
= backup
)
760 patch
.set_top(commit_id
, backup
= backup
)
761 patch
.set_description(descr
)
762 patch
.set_authname(author_name
)
763 patch
.set_authemail(author_email
)
764 patch
.set_authdate(author_date
)
765 patch
.set_commname(committer_name
)
766 patch
.set_commemail(committer_email
)
769 self
.log_patch(patch
, log
)
773 def undo_refresh(self
):
774 """Undo the patch boundaries changes caused by 'refresh'
776 name
= self
.get_current()
779 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
780 old_bottom
= patch
.get_old_bottom()
781 old_top
= patch
.get_old_top()
783 # the bottom of the patch is not changed by refresh. If the
784 # old_bottom is different, there wasn't any previous 'refresh'
785 # command (probably only a 'push')
786 if old_bottom
!= patch
.get_bottom() or old_top
== patch
.get_top():
787 raise StackException
, 'No undo information available'
789 git
.reset(tree_id
= old_top
, check_out
= False)
790 if patch
.restore_old_boundaries():
791 self
.log_patch(patch
, 'undo')
793 def new_patch(self
, name
, message
= None, can_edit
= True,
794 unapplied
= False, show_patch
= False,
795 top
= None, bottom
= None,
796 author_name
= None, author_email
= None, author_date
= None,
797 committer_name
= None, committer_email
= None,
798 before_existing
= False, refresh
= True):
799 """Creates a new patch
803 self
.__patch
_name
_valid
(name
)
804 if self
.patch_applied(name
) or self
.patch_unapplied(name
):
805 raise StackException
, 'Patch "%s" already exists' % name
807 if not message
and can_edit
:
810 'Please enter the description for the patch above.',
815 head
= git
.get_head()
818 name
= make_patch_name(descr
, self
.patch_exists
)
820 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
824 patch
.set_bottom(bottom
)
826 patch
.set_bottom(head
)
832 patch
.set_description(descr
)
833 patch
.set_authname(author_name
)
834 patch
.set_authemail(author_email
)
835 patch
.set_authdate(author_date
)
836 patch
.set_commname(committer_name
)
837 patch
.set_commemail(committer_email
)
840 self
.log_patch(patch
, 'new')
842 patches
= [patch
.get_name()] + self
.get_unapplied()
844 f
= file(self
.__unapplied
_file
, 'w+')
845 f
.writelines([line
+ '\n' for line
in patches
])
847 elif before_existing
:
848 self
.log_patch(patch
, 'new')
850 insert_string(self
.__applied
_file
, patch
.get_name())
852 append_string(self
.__applied
_file
, patch
.get_name())
854 self
.refresh_patch(cache_update
= False, log
= 'new')
858 def delete_patch(self
, name
):
861 self
.__patch
_name
_valid
(name
)
862 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
864 if self
.__patch
_is
_current
(patch
):
866 elif self
.patch_applied(name
):
867 raise StackException
, 'Cannot remove an applied patch, "%s", ' \
868 'which is not current' % name
869 elif not name
in self
.get_unapplied():
870 raise StackException
, 'Unknown patch "%s"' % name
872 # save the commit id to a trash file
873 write_string(os
.path
.join(self
.__trash
_dir
, name
), patch
.get_top())
877 unapplied
= self
.get_unapplied()
878 unapplied
.remove(name
)
879 f
= file(self
.__unapplied
_file
, 'w+')
880 f
.writelines([line
+ '\n' for line
in unapplied
])
883 if self
.patch_hidden(name
):
884 self
.unhide_patch(name
)
886 def forward_patches(self
, names
):
887 """Try to fast-forward an array of patches.
889 On return, patches in names[0:returned_value] have been pushed on the
890 stack. Apply the rest with push_patch
892 unapplied
= self
.get_unapplied()
898 assert(name
in unapplied
)
900 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
903 bottom
= patch
.get_bottom()
904 top
= patch
.get_top()
906 # top != bottom always since we have a commit for each patch
908 # reset the backup information. No logging since the
909 # patch hasn't changed
910 patch
.set_bottom(head
, backup
= True)
911 patch
.set_top(top
, backup
= True)
914 head_tree
= git
.get_commit(head
).get_tree()
915 bottom_tree
= git
.get_commit(bottom
).get_tree()
916 if head_tree
== bottom_tree
:
917 # We must just reparent this patch and create a new commit
919 descr
= patch
.get_description()
920 author_name
= patch
.get_authname()
921 author_email
= patch
.get_authemail()
922 author_date
= patch
.get_authdate()
923 committer_name
= patch
.get_commname()
924 committer_email
= patch
.get_commemail()
926 top_tree
= git
.get_commit(top
).get_tree()
928 top
= git
.commit(message
= descr
, parents
= [head
],
929 cache_update
= False,
932 author_name
= author_name
,
933 author_email
= author_email
,
934 author_date
= author_date
,
935 committer_name
= committer_name
,
936 committer_email
= committer_email
)
938 patch
.set_bottom(head
, backup
= True)
939 patch
.set_top(top
, backup
= True)
941 self
.log_patch(patch
, 'push(f)')
944 # stop the fast-forwarding, must do a real merge
948 unapplied
.remove(name
)
955 append_strings(self
.__applied
_file
, names
[0:forwarded
])
957 f
= file(self
.__unapplied
_file
, 'w+')
958 f
.writelines([line
+ '\n' for line
in unapplied
])
963 def merged_patches(self
, names
):
964 """Test which patches were merged upstream by reverse-applying
965 them in reverse order. The function returns the list of
966 patches detected to have been applied. The state of the tree
967 is restored to the original one
969 patches
= [Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
975 if git
.apply_diff(p
.get_top(), p
.get_bottom()):
976 merged
.append(p
.get_name())
983 def push_patch(self
, name
, empty
= False):
984 """Pushes a patch on the stack
986 unapplied
= self
.get_unapplied()
987 assert(name
in unapplied
)
989 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
991 head
= git
.get_head()
992 bottom
= patch
.get_bottom()
993 top
= patch
.get_top()
998 # top != bottom always since we have a commit for each patch
1000 # just make an empty patch (top = bottom = HEAD). This
1001 # option is useful to allow undoing already merged
1002 # patches. The top is updated by refresh_patch since we
1003 # need an empty commit
1004 patch
.set_bottom(head
, backup
= True)
1005 patch
.set_top(head
, backup
= True)
1007 elif head
== bottom
:
1008 # reset the backup information. No need for logging
1009 patch
.set_bottom(bottom
, backup
= True)
1010 patch
.set_top(top
, backup
= True)
1014 # new patch needs to be refreshed.
1015 # The current patch is empty after merge.
1016 patch
.set_bottom(head
, backup
= True)
1017 patch
.set_top(head
, backup
= True)
1019 # Try the fast applying first. If this fails, fall back to the
1021 if not git
.apply_diff(bottom
, top
):
1022 # if git.apply_diff() fails, the patch requires a diff3
1023 # merge and can be reported as modified
1026 # merge can fail but the patch needs to be pushed
1028 git
.merge(bottom
, head
, top
, recursive
= True)
1029 except git
.GitException
, ex
:
1030 print >> sys
.stderr
, \
1031 'The merge failed during "push". ' \
1032 'Use "refresh" after fixing the conflicts or ' \
1033 'revert the operation with "push --undo".'
1035 append_string(self
.__applied
_file
, name
)
1037 unapplied
.remove(name
)
1038 f
= file(self
.__unapplied
_file
, 'w+')
1039 f
.writelines([line
+ '\n' for line
in unapplied
])
1042 # head == bottom case doesn't need to refresh the patch
1043 if empty
or head
!= bottom
:
1045 # if the merge was OK and no conflicts, just refresh the patch
1046 # The GIT cache was already updated by the merge operation
1051 self
.refresh_patch(cache_update
= False, log
= log
)
1053 # we store the correctly merged files only for
1054 # tracking the conflict history. Note that the
1055 # git.merge() operations should always leave the index
1056 # in a valid state (i.e. only stage 0 files)
1057 self
.refresh_patch(cache_update
= False, log
= 'push(c)')
1058 raise StackException
, str(ex
)
1062 def undo_push(self
):
1063 name
= self
.get_current()
1066 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
1067 old_bottom
= patch
.get_old_bottom()
1068 old_top
= patch
.get_old_top()
1070 # the top of the patch is changed by a push operation only
1071 # together with the bottom (otherwise the top was probably
1072 # modified by 'refresh'). If they are both unchanged, there
1073 # was a fast forward
1074 if old_bottom
== patch
.get_bottom() and old_top
!= patch
.get_top():
1075 raise StackException
, 'No undo information available'
1078 self
.pop_patch(name
)
1079 ret
= patch
.restore_old_boundaries()
1081 self
.log_patch(patch
, 'undo')
1085 def pop_patch(self
, name
, keep
= False):
1086 """Pops the top patch from the stack
1088 applied
= self
.get_applied()
1090 assert(name
in applied
)
1092 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
1094 if git
.get_head_file() == self
.get_branch():
1095 if keep
and not git
.apply_diff(git
.get_head(), patch
.get_bottom()):
1096 raise StackException(
1097 'Failed to pop patches while preserving the local changes')
1098 git
.switch(patch
.get_bottom(), keep
)
1100 git
.set_branch(self
.get_branch(), patch
.get_bottom())
1102 # save the new applied list
1103 idx
= applied
.index(name
) + 1
1105 popped
= applied
[:idx
]
1107 unapplied
= popped
+ self
.get_unapplied()
1109 f
= file(self
.__unapplied
_file
, 'w+')
1110 f
.writelines([line
+ '\n' for line
in unapplied
])
1116 f
= file(self
.__applied
_file
, 'w+')
1117 f
.writelines([line
+ '\n' for line
in applied
])
1120 def empty_patch(self
, name
):
1121 """Returns True if the patch is empty
1123 self
.__patch
_name
_valid
(name
)
1124 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
1125 bottom
= patch
.get_bottom()
1126 top
= patch
.get_top()
1130 elif git
.get_commit(top
).get_tree() \
1131 == git
.get_commit(bottom
).get_tree():
1136 def rename_patch(self
, oldname
, newname
):
1137 self
.__patch
_name
_valid
(newname
)
1139 applied
= self
.get_applied()
1140 unapplied
= self
.get_unapplied()
1142 if oldname
== newname
:
1143 raise StackException
, '"To" name and "from" name are the same'
1145 if newname
in applied
or newname
in unapplied
:
1146 raise StackException
, 'Patch "%s" already exists' % newname
1148 if self
.patch_hidden(oldname
):
1149 self
.unhide_patch(oldname
)
1150 self
.hide_patch(newname
)
1152 if oldname
in unapplied
:
1153 Patch(oldname
, self
.__patch
_dir
, self
.__refs
_dir
).rename(newname
)
1154 unapplied
[unapplied
.index(oldname
)] = newname
1156 f
= file(self
.__unapplied
_file
, 'w+')
1157 f
.writelines([line
+ '\n' for line
in unapplied
])
1159 elif oldname
in applied
:
1160 Patch(oldname
, self
.__patch
_dir
, self
.__refs
_dir
).rename(newname
)
1162 applied
[applied
.index(oldname
)] = newname
1164 f
= file(self
.__applied
_file
, 'w+')
1165 f
.writelines([line
+ '\n' for line
in applied
])
1168 raise StackException
, 'Unknown patch "%s"' % oldname
1170 def log_patch(self
, patch
, message
):
1171 """Generate a log commit for a patch
1173 top
= git
.get_commit(patch
.get_top())
1174 msg
= '%s\t%s' % (message
, top
.get_id_hash())
1176 old_log
= patch
.get_log()
1182 log
= git
.commit(message
= msg
, parents
= parents
,
1183 cache_update
= False, tree_id
= top
.get_tree(),
1187 def hide_patch(self
, name
):
1188 """Add the patch to the hidden list.
1190 if not self
.patch_exists(name
):
1191 raise StackException
, 'Unknown patch "%s"' % name
1192 elif self
.patch_hidden(name
):
1193 raise StackException
, 'Patch "%s" already hidden' % name
1195 append_string(self
.__hidden
_file
, name
)
1197 def unhide_patch(self
, name
):
1198 """Add the patch to the hidden list.
1200 if not self
.patch_exists(name
):
1201 raise StackException
, 'Unknown patch "%s"' % name
1202 hidden
= self
.get_hidden()
1203 if not name
in hidden
:
1204 raise StackException
, 'Patch "%s" not hidden' % name
1208 f
= file(self
.__hidden
_file
, 'w+')
1209 f
.writelines([line
+ '\n' for line
in hidden
])