1 """Basic quilt-like functionality
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 from stgit
.utils
import *
24 from stgit
import git
, basedir
, templates
25 from stgit
.config
import config
28 # stack exception class
29 class StackException(Exception):
34 self
.should_print
= True
35 def __call__(self
, x
, until_test
, prefix
):
37 self
.should_print
= False
39 return x
[0:len(prefix
)] != prefix
45 __comment_prefix
= 'STG:'
46 __patch_prefix
= 'STG_PATCH:'
48 def __clean_comments(f
):
49 """Removes lines marked for status in a commit file
53 # remove status-prefixed lines
56 patch_filter
= FilterUntil()
57 until_test
= lambda t
: t
== (__patch_prefix
+ '\n')
58 lines
= [l
for l
in lines
if patch_filter(l
, until_test
, __comment_prefix
)]
60 # remove empty lines at the end
61 while len(lines
) != 0 and lines
[-1] == '\n':
64 f
.seek(0); f
.truncate()
67 def edit_file(series
, line
, comment
, show_patch
= True):
68 fname
= '.stgitmsg.txt'
69 tmpl
= templates
.get_template('patchdescr.tmpl')
78 print >> f
, __comment_prefix
, comment
79 print >> f
, __comment_prefix
, \
80 'Lines prefixed with "%s" will be automatically removed.' \
82 print >> f
, __comment_prefix
, \
83 'Trailing empty lines will be automatically removed.'
86 print >> f
, __patch_prefix
87 # series.get_patch(series.get_current()).get_top()
88 git
.diff([], series
.get_patch(series
.get_current()).get_bottom(), None, f
)
90 #Vim modeline must be near the end.
91 print >> f
, __comment_prefix
, 'vi: set textwidth=75 filetype=diff nobackup:'
112 """An object with stgit-like properties stored as files in a directory
114 def _set_dir(self
, dir):
119 def create_empty_field(self
, name
):
120 create_empty_file(os
.path
.join(self
.__dir
, name
))
122 def _get_field(self
, name
, multiline
= False):
123 id_file
= os
.path
.join(self
.__dir
, name
)
124 if os
.path
.isfile(id_file
):
125 line
= read_string(id_file
, multiline
)
133 def _set_field(self
, name
, value
, multiline
= False):
134 fname
= os
.path
.join(self
.__dir
, name
)
135 if value
and value
!= '':
136 write_string(fname
, value
, multiline
)
137 elif os
.path
.isfile(fname
):
141 class Patch(StgitObject
):
142 """Basic patch implementation
144 def __init__(self
, name
, series_dir
, refs_dir
):
145 self
.__series
_dir
= series_dir
147 self
._set
_dir
(os
.path
.join(self
.__series
_dir
, self
.__name
))
148 self
.__refs
_dir
= refs_dir
149 self
.__top
_ref
_file
= os
.path
.join(self
.__refs
_dir
, self
.__name
)
150 self
.__log
_ref
_file
= os
.path
.join(self
.__refs
_dir
,
151 self
.__name
+ '.log')
154 os
.mkdir(self
._dir
())
155 self
.create_empty_field('bottom')
156 self
.create_empty_field('top')
159 for f
in os
.listdir(self
._dir
()):
160 os
.remove(os
.path
.join(self
._dir
(), f
))
161 os
.rmdir(self
._dir
())
162 os
.remove(self
.__top
_ref
_file
)
163 if os
.path
.exists(self
.__log
_ref
_file
):
164 os
.remove(self
.__log
_ref
_file
)
169 def rename(self
, newname
):
171 old_top_ref_file
= self
.__top
_ref
_file
172 old_log_ref_file
= self
.__log
_ref
_file
173 self
.__name
= newname
174 self
._set
_dir
(os
.path
.join(self
.__series
_dir
, self
.__name
))
175 self
.__top
_ref
_file
= os
.path
.join(self
.__refs
_dir
, self
.__name
)
176 self
.__log
_ref
_file
= os
.path
.join(self
.__refs
_dir
,
177 self
.__name
+ '.log')
179 os
.rename(olddir
, self
._dir
())
180 os
.rename(old_top_ref_file
, self
.__top
_ref
_file
)
181 if os
.path
.exists(old_log_ref_file
):
182 os
.rename(old_log_ref_file
, self
.__log
_ref
_file
)
184 def __update_top_ref(self
, ref
):
185 write_string(self
.__top
_ref
_file
, ref
)
187 def __update_log_ref(self
, ref
):
188 write_string(self
.__log
_ref
_file
, ref
)
190 def update_top_ref(self
):
193 self
.__update
_top
_ref
(top
)
195 def get_old_bottom(self
):
196 return self
._get
_field
('bottom.old')
198 def get_bottom(self
):
199 return self
._get
_field
('bottom')
201 def set_bottom(self
, value
, backup
= False):
203 curr
= self
._get
_field
('bottom')
204 self
._set
_field
('bottom.old', curr
)
205 self
._set
_field
('bottom', value
)
207 def get_old_top(self
):
208 return self
._get
_field
('top.old')
211 return self
._get
_field
('top')
213 def set_top(self
, value
, backup
= False):
215 curr
= self
._get
_field
('top')
216 self
._set
_field
('top.old', curr
)
217 self
._set
_field
('top', value
)
218 self
.__update
_top
_ref
(value
)
220 def restore_old_boundaries(self
):
221 bottom
= self
._get
_field
('bottom.old')
222 top
= self
._get
_field
('top.old')
225 self
._set
_field
('bottom', bottom
)
226 self
._set
_field
('top', top
)
227 self
.__update
_top
_ref
(top
)
232 def get_description(self
):
233 return self
._get
_field
('description', True)
235 def set_description(self
, line
):
236 self
._set
_field
('description', line
, True)
238 def get_authname(self
):
239 return self
._get
_field
('authname')
241 def set_authname(self
, name
):
242 self
._set
_field
('authname', name
or git
.author().name
)
244 def get_authemail(self
):
245 return self
._get
_field
('authemail')
247 def set_authemail(self
, email
):
248 self
._set
_field
('authemail', email
or git
.author().email
)
250 def get_authdate(self
):
251 return self
._get
_field
('authdate')
253 def set_authdate(self
, date
):
254 self
._set
_field
('authdate', date
or git
.author().date
)
256 def get_commname(self
):
257 return self
._get
_field
('commname')
259 def set_commname(self
, name
):
260 self
._set
_field
('commname', name
or git
.committer().name
)
262 def get_commemail(self
):
263 return self
._get
_field
('commemail')
265 def set_commemail(self
, email
):
266 self
._set
_field
('commemail', email
or git
.committer().email
)
269 return self
._get
_field
('log')
271 def set_log(self
, value
, backup
= False):
272 self
._set
_field
('log', value
)
273 self
.__update
_log
_ref
(value
)
276 class Series(StgitObject
):
277 """Class including the operations on series
279 def __init__(self
, name
= None):
280 """Takes a series name as the parameter.
286 self
.__name
= git
.get_head_file()
287 self
.__base
_dir
= basedir
.get()
288 except git
.GitException
, ex
:
289 raise StackException
, 'GIT tree not initialised: %s' % ex
291 self
._set
_dir
(os
.path
.join(self
.__base
_dir
, 'patches', self
.__name
))
292 self
.__refs
_dir
= os
.path
.join(self
.__base
_dir
, 'refs', 'patches',
294 self
.__base
_file
= os
.path
.join(self
.__base
_dir
, 'refs', 'bases',
297 self
.__applied
_file
= os
.path
.join(self
._dir
(), 'applied')
298 self
.__unapplied
_file
= os
.path
.join(self
._dir
(), 'unapplied')
299 self
.__hidden
_file
= os
.path
.join(self
._dir
(), 'hidden')
300 self
.__current
_file
= os
.path
.join(self
._dir
(), 'current')
301 self
.__descr
_file
= os
.path
.join(self
._dir
(), 'description')
303 # where this series keeps its patches
304 self
.__patch
_dir
= os
.path
.join(self
._dir
(), 'patches')
305 if not os
.path
.isdir(self
.__patch
_dir
):
306 self
.__patch
_dir
= self
._dir
()
308 # if no __refs_dir, create and populate it (upgrade old repositories)
309 if self
.is_initialised() and not os
.path
.isdir(self
.__refs
_dir
):
310 os
.makedirs(self
.__refs
_dir
)
311 for patch
in self
.get_applied() + self
.get_unapplied():
312 self
.get_patch(patch
).update_top_ref()
315 self
.__trash
_dir
= os
.path
.join(self
._dir
(), 'trash')
316 if self
.is_initialised() and not os
.path
.isdir(self
.__trash
_dir
):
317 os
.makedirs(self
.__trash
_dir
)
319 def __patch_name_valid(self
, name
):
320 """Raise an exception if the patch name is not valid.
322 if not name
or re
.search('[^\w.-]', name
):
323 raise StackException
, 'Invalid patch name: "%s"' % name
325 def get_branch(self
):
326 """Return the branch name for the Series object
330 def __set_current(self
, name
):
331 """Sets the topmost patch
333 self
._set
_field
('current', name
)
335 def get_patch(self
, name
):
336 """Return a Patch object for the given name
338 return Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
340 def get_current_patch(self
):
341 """Return a Patch object representing the topmost patch, or
342 None if there is no such patch."""
343 crt
= self
.get_current()
346 return Patch(crt
, self
.__patch
_dir
, self
.__refs
_dir
)
348 def get_current(self
):
349 """Return the name of the topmost patch, or None if there is
351 name
= self
._get
_field
('current')
357 def get_applied(self
):
358 if not os
.path
.isfile(self
.__applied
_file
):
359 raise StackException
, 'Branch "%s" not initialised' % self
.__name
360 f
= file(self
.__applied
_file
)
361 names
= [line
.strip() for line
in f
.readlines()]
365 def get_unapplied(self
):
366 if not os
.path
.isfile(self
.__unapplied
_file
):
367 raise StackException
, 'Branch "%s" not initialised' % self
.__name
368 f
= file(self
.__unapplied
_file
)
369 names
= [line
.strip() for line
in f
.readlines()]
373 def get_hidden(self
):
374 if not os
.path
.isfile(self
.__hidden
_file
):
376 f
= file(self
.__hidden
_file
)
377 names
= [line
.strip() for line
in f
.readlines()]
381 def get_base_file(self
):
382 self
.__begin
_stack
_check
()
383 return self
.__base
_file
386 return read_string(self
.get_base_file())
389 """Return the head of the branch
391 crt
= self
.get_current_patch()
395 return self
.get_base()
397 def get_protected(self
):
398 return os
.path
.isfile(os
.path
.join(self
._dir
(), 'protected'))
401 protect_file
= os
.path
.join(self
._dir
(), 'protected')
402 if not os
.path
.isfile(protect_file
):
403 create_empty_file(protect_file
)
406 protect_file
= os
.path
.join(self
._dir
(), 'protected')
407 if os
.path
.isfile(protect_file
):
408 os
.remove(protect_file
)
410 def get_description(self
):
411 return self
._get
_field
('description') or ''
413 def set_description(self
, line
):
414 self
._set
_field
('description', line
)
416 def get_parent_remote(self
):
417 value
= config
.get('branch.%s.remote' % self
.__name
)
420 elif 'origin' in git
.remotes_list():
421 print 'Notice: no parent remote declared for stack "%s", ' \
422 'defaulting to "origin". Consider setting "branch.%s.remote" ' \
423 'and "branch.%s.merge" with "git repo-config".' \
424 % (self
.__name
, self
.__name
, self
.__name
)
427 raise StackException
, 'Cannot find a parent remote for "%s"' % self
.__name
429 def __set_parent_remote(self
, remote
):
430 value
= config
.set('branch.%s.remote' % self
.__name
, remote
)
432 def get_parent_branch(self
):
433 value
= config
.get('branch.%s.stgit.parentbranch' % self
.__name
)
436 elif git
.rev_parse('heads/origin'):
437 print 'Notice: no parent branch declared for stack "%s", ' \
438 'defaulting to "heads/origin". Consider setting ' \
439 '"branch.%s.stgit.parentbranch" with "git repo-config".' \
440 % (self
.__name
, self
.__name
)
441 return 'heads/origin'
443 raise StackException
, 'Cannot find a parent branch for "%s"' % self
.__name
445 def __set_parent_branch(self
, name
):
446 if config
.get('branch.%s.remote' % self
.__name
):
447 # Never set merge if remote is not set to avoid
448 # possibly-erroneous lookups into 'origin'
449 config
.set('branch.%s.merge' % self
.__name
, name
)
450 config
.set('branch.%s.stgit.parentbranch' % self
.__name
, name
)
452 def set_parent(self
, remote
, localbranch
):
453 # policy: record local branches as remote='.'
454 recordremote
= remote
or '.'
456 self
.__set
_parent
_remote
(recordremote
)
457 self
.__set
_parent
_branch
(localbranch
)
458 # We'll enforce this later
460 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
462 def __patch_is_current(self
, patch
):
463 return patch
.get_name() == self
.get_current()
465 def patch_applied(self
, name
):
466 """Return true if the patch exists in the applied list
468 return name
in self
.get_applied()
470 def patch_unapplied(self
, name
):
471 """Return true if the patch exists in the unapplied list
473 return name
in self
.get_unapplied()
475 def patch_hidden(self
, name
):
476 """Return true if the patch is hidden.
478 return name
in self
.get_hidden()
480 def patch_exists(self
, name
):
481 """Return true if there is a patch with the given name, false
483 return self
.patch_applied(name
) or self
.patch_unapplied(name
)
485 def __begin_stack_check(self
):
486 """Save the current HEAD into .git/refs/heads/base if the stack
489 if len(self
.get_applied()) == 0:
490 head
= git
.get_head()
491 write_string(self
.__base
_file
, head
)
493 def __end_stack_check(self
):
494 """Remove .git/refs/heads/base if the stack is empty.
495 This warning should never happen
497 if len(self
.get_applied()) == 0 \
498 and read_string(self
.__base
_file
) != git
.get_head():
499 print 'Warning: stack empty but the HEAD and base are different'
501 def head_top_equal(self
):
502 """Return true if the head and the top are the same
504 crt
= self
.get_current_patch()
506 # we don't care, no patches applied
508 return git
.get_head() == crt
.get_top()
510 def is_initialised(self
):
511 """Checks if series is already initialised
513 return os
.path
.isdir(self
.__patch
_dir
)
515 def init(self
, create_at
=False, parent_remote
=None, parent_branch
=None):
516 """Initialises the stgit series
518 if os
.path
.exists(self
.__patch
_dir
):
519 raise StackException
, self
.__patch
_dir
+ ' already exists'
520 if os
.path
.exists(self
.__refs
_dir
):
521 raise StackException
, self
.__refs
_dir
+ ' already exists'
522 if os
.path
.exists(self
.__base
_file
):
523 raise StackException
, self
.__base
_file
+ ' already exists'
525 if (create_at
!=False):
526 git
.create_branch(self
.__name
, create_at
)
528 os
.makedirs(self
.__patch
_dir
)
530 self
.set_parent(parent_remote
, parent_branch
)
532 create_dirs(os
.path
.join(self
.__base
_dir
, 'refs', 'bases'))
534 self
.create_empty_field('applied')
535 self
.create_empty_field('unapplied')
536 self
.create_empty_field('description')
537 os
.makedirs(os
.path
.join(self
._dir
(), 'patches'))
538 os
.makedirs(self
.__refs
_dir
)
539 self
.__begin
_stack
_check
()
540 self
._set
_field
('orig-base', git
.get_head())
543 """Either convert to use a separate patch directory, or
544 unconvert to place the patches in the same directory with
547 if self
.__patch
_dir
== self
._dir
():
548 print 'Converting old-style to new-style...',
551 self
.__patch
_dir
= os
.path
.join(self
._dir
(), 'patches')
552 os
.makedirs(self
.__patch
_dir
)
554 for p
in self
.get_applied() + self
.get_unapplied():
555 src
= os
.path
.join(self
._dir
(), p
)
556 dest
= os
.path
.join(self
.__patch
_dir
, p
)
562 print 'Converting new-style to old-style...',
565 for p
in self
.get_applied() + self
.get_unapplied():
566 src
= os
.path
.join(self
.__patch
_dir
, p
)
567 dest
= os
.path
.join(self
._dir
(), p
)
570 if not os
.listdir(self
.__patch
_dir
):
571 os
.rmdir(self
.__patch
_dir
)
574 print 'Patch directory %s is not empty.' % self
.__patch
_dir
576 self
.__patch
_dir
= self
._dir
()
578 def rename(self
, to_name
):
581 to_stack
= Series(to_name
)
583 if to_stack
.is_initialised():
584 raise StackException
, '"%s" already exists' % to_stack
.get_branch()
585 if os
.path
.exists(to_stack
.__base
_file
):
586 os
.remove(to_stack
.__base
_file
)
588 git
.rename_branch(self
.__name
, to_name
)
590 if os
.path
.isdir(self
._dir
()):
591 rename(os
.path
.join(self
.__base
_dir
, 'patches'),
592 self
.__name
, to_stack
.__name
)
593 if os
.path
.exists(self
.__base
_file
):
594 rename(os
.path
.join(self
.__base
_dir
, 'refs', 'bases'),
595 self
.__name
, to_stack
.__name
)
596 if os
.path
.exists(self
.__refs
_dir
):
597 rename(os
.path
.join(self
.__base
_dir
, 'refs', 'patches'),
598 self
.__name
, to_stack
.__name
)
600 # Rename the config section
601 config
.rename_section("branch.%s" % self
.__name
,
602 "branch.%s" % to_name
)
604 self
.__init
__(to_name
)
606 def clone(self
, target_series
):
610 # allow cloning of branches not under StGIT control
611 base
= self
.get_base()
613 base
= git
.get_head()
614 Series(target_series
).init(create_at
= base
)
615 new_series
= Series(target_series
)
617 # generate an artificial description file
618 new_series
.set_description('clone of "%s"' % self
.__name
)
620 # clone self's entire series as unapplied patches
622 # allow cloning of branches not under StGIT control
623 applied
= self
.get_applied()
624 unapplied
= self
.get_unapplied()
625 patches
= applied
+ unapplied
628 patches
= applied
= unapplied
= []
630 patch
= self
.get_patch(p
)
631 new_series
.new_patch(p
, message
= patch
.get_description(),
632 can_edit
= False, unapplied
= True,
633 bottom
= patch
.get_bottom(),
634 top
= patch
.get_top(),
635 author_name
= patch
.get_authname(),
636 author_email
= patch
.get_authemail(),
637 author_date
= patch
.get_authdate())
639 # fast forward the cloned series to self's top
640 new_series
.forward_patches(applied
)
642 # Clone parent informations
643 value
= config
.get('branch.%s.remote' % self
.__name
)
645 config
.set('branch.%s.remote' % target_series
, value
)
647 value
= config
.get('branch.%s.merge' % self
.__name
)
649 config
.set('branch.%s.merge' % target_series
, value
)
651 value
= config
.get('branch.%s.stgit.parentbranch' % self
.__name
)
653 config
.set('branch.%s.stgit.parentbranch' % target_series
, value
)
655 def delete(self
, force
= False):
656 """Deletes an stgit series
658 if self
.is_initialised():
659 patches
= self
.get_unapplied() + self
.get_applied()
660 if not force
and patches
:
661 raise StackException
, \
662 'Cannot delete: the series still contains patches'
664 Patch(p
, self
.__patch
_dir
, self
.__refs
_dir
).delete()
666 # remove the trash directory
667 for fname
in os
.listdir(self
.__trash
_dir
):
669 os
.rmdir(self
.__trash
_dir
)
671 # FIXME: find a way to get rid of those manual removals
672 # (move functionality to StgitObject ?)
673 if os
.path
.exists(self
.__applied
_file
):
674 os
.remove(self
.__applied
_file
)
675 if os
.path
.exists(self
.__unapplied
_file
):
676 os
.remove(self
.__unapplied
_file
)
677 if os
.path
.exists(self
.__hidden
_file
):
678 os
.remove(self
.__hidden
_file
)
679 if os
.path
.exists(self
.__current
_file
):
680 os
.remove(self
.__current
_file
)
681 if os
.path
.exists(self
.__descr
_file
):
682 os
.remove(self
.__descr
_file
)
683 if os
.path
.exists(self
._dir
()+'/orig-base'):
684 os
.remove(self
._dir
()+'/orig-base')
686 if not os
.listdir(self
.__patch
_dir
):
687 os
.rmdir(self
.__patch
_dir
)
689 print 'Patch directory %s is not empty.' % self
.__patch
_dir
692 os
.removedirs(self
._dir
())
694 raise StackException
, 'Series directory %s is not empty.' % self
._dir
()
697 os
.removedirs(self
.__refs
_dir
)
699 print 'Refs directory %s is not empty.' % self
.__refs
_dir
701 if os
.path
.exists(self
.__base
_file
):
702 remove_file_and_dirs(
703 os
.path
.join(self
.__base
_dir
, 'refs', 'bases'), self
.__name
)
705 def refresh_patch(self
, files
= None, message
= None, edit
= False,
708 author_name
= None, author_email
= None,
710 committer_name
= None, committer_email
= None,
711 backup
= False, sign_str
= None, log
= 'refresh'):
712 """Generates a new commit for the given patch
714 name
= self
.get_current()
716 raise StackException
, 'No patches applied'
718 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
720 descr
= patch
.get_description()
721 if not (message
or descr
):
727 if not message
and edit
:
728 descr
= edit_file(self
, descr
.rstrip(), \
729 'Please edit the description for patch "%s" ' \
730 'above.' % name
, show_patch
)
733 author_name
= patch
.get_authname()
735 author_email
= patch
.get_authemail()
737 author_date
= patch
.get_authdate()
738 if not committer_name
:
739 committer_name
= patch
.get_commname()
740 if not committer_email
:
741 committer_email
= patch
.get_commemail()
744 descr
= descr
.rstrip()
745 if descr
.find("\nSigned-off-by:") < 0 \
746 and descr
.find("\nAcked-by:") < 0:
749 descr
= '%s\n%s: %s <%s>\n' % (descr
, sign_str
,
750 committer_name
, committer_email
)
752 bottom
= patch
.get_bottom()
754 commit_id
= git
.commit(files
= files
,
755 message
= descr
, parents
= [bottom
],
756 cache_update
= cache_update
,
758 author_name
= author_name
,
759 author_email
= author_email
,
760 author_date
= author_date
,
761 committer_name
= committer_name
,
762 committer_email
= committer_email
)
764 patch
.set_bottom(bottom
, backup
= backup
)
765 patch
.set_top(commit_id
, backup
= backup
)
766 patch
.set_description(descr
)
767 patch
.set_authname(author_name
)
768 patch
.set_authemail(author_email
)
769 patch
.set_authdate(author_date
)
770 patch
.set_commname(committer_name
)
771 patch
.set_commemail(committer_email
)
774 self
.log_patch(patch
, log
)
778 def undo_refresh(self
):
779 """Undo the patch boundaries changes caused by 'refresh'
781 name
= self
.get_current()
784 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
785 old_bottom
= patch
.get_old_bottom()
786 old_top
= patch
.get_old_top()
788 # the bottom of the patch is not changed by refresh. If the
789 # old_bottom is different, there wasn't any previous 'refresh'
790 # command (probably only a 'push')
791 if old_bottom
!= patch
.get_bottom() or old_top
== patch
.get_top():
792 raise StackException
, 'No undo information available'
794 git
.reset(tree_id
= old_top
, check_out
= False)
795 if patch
.restore_old_boundaries():
796 self
.log_patch(patch
, 'undo')
798 def new_patch(self
, name
, message
= None, can_edit
= True,
799 unapplied
= False, show_patch
= False,
800 top
= None, bottom
= None,
801 author_name
= None, author_email
= None, author_date
= None,
802 committer_name
= None, committer_email
= None,
803 before_existing
= False, refresh
= True):
804 """Creates a new patch
806 self
.__patch
_name
_valid
(name
)
808 if self
.patch_applied(name
) or self
.patch_unapplied(name
):
809 raise StackException
, 'Patch "%s" already exists' % name
811 if not message
and can_edit
:
812 descr
= edit_file(self
, None, \
813 'Please enter the description for patch "%s" ' \
814 'above.' % name
, show_patch
)
818 head
= git
.get_head()
820 self
.__begin
_stack
_check
()
822 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
826 patch
.set_bottom(bottom
)
828 patch
.set_bottom(head
)
834 patch
.set_description(descr
)
835 patch
.set_authname(author_name
)
836 patch
.set_authemail(author_email
)
837 patch
.set_authdate(author_date
)
838 patch
.set_commname(committer_name
)
839 patch
.set_commemail(committer_email
)
842 self
.log_patch(patch
, 'new')
844 patches
= [patch
.get_name()] + self
.get_unapplied()
846 f
= file(self
.__unapplied
_file
, 'w+')
847 f
.writelines([line
+ '\n' for line
in patches
])
849 elif before_existing
:
850 self
.log_patch(patch
, 'new')
852 insert_string(self
.__applied
_file
, patch
.get_name())
853 if not self
.get_current():
854 self
.__set
_current
(name
)
856 append_string(self
.__applied
_file
, patch
.get_name())
857 self
.__set
_current
(name
)
859 self
.refresh_patch(cache_update
= False, log
= 'new')
861 def delete_patch(self
, name
):
864 self
.__patch
_name
_valid
(name
)
865 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
867 if self
.__patch
_is
_current
(patch
):
869 elif self
.patch_applied(name
):
870 raise StackException
, 'Cannot remove an applied patch, "%s", ' \
871 'which is not current' % name
872 elif not name
in self
.get_unapplied():
873 raise StackException
, 'Unknown patch "%s"' % name
875 # save the commit id to a trash file
876 write_string(os
.path
.join(self
.__trash
_dir
, name
), patch
.get_top())
880 unapplied
= self
.get_unapplied()
881 unapplied
.remove(name
)
882 f
= file(self
.__unapplied
_file
, 'w+')
883 f
.writelines([line
+ '\n' for line
in unapplied
])
886 if self
.patch_hidden(name
):
887 self
.unhide_patch(name
)
889 self
.__begin
_stack
_check
()
891 def forward_patches(self
, names
):
892 """Try to fast-forward an array of patches.
894 On return, patches in names[0:returned_value] have been pushed on the
895 stack. Apply the rest with push_patch
897 unapplied
= self
.get_unapplied()
898 self
.__begin
_stack
_check
()
904 assert(name
in unapplied
)
906 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
909 bottom
= patch
.get_bottom()
910 top
= patch
.get_top()
912 # top != bottom always since we have a commit for each patch
914 # reset the backup information. No logging since the
915 # patch hasn't changed
916 patch
.set_bottom(head
, backup
= True)
917 patch
.set_top(top
, backup
= True)
920 head_tree
= git
.get_commit(head
).get_tree()
921 bottom_tree
= git
.get_commit(bottom
).get_tree()
922 if head_tree
== bottom_tree
:
923 # We must just reparent this patch and create a new commit
925 descr
= patch
.get_description()
926 author_name
= patch
.get_authname()
927 author_email
= patch
.get_authemail()
928 author_date
= patch
.get_authdate()
929 committer_name
= patch
.get_commname()
930 committer_email
= patch
.get_commemail()
932 top_tree
= git
.get_commit(top
).get_tree()
934 top
= git
.commit(message
= descr
, parents
= [head
],
935 cache_update
= False,
938 author_name
= author_name
,
939 author_email
= author_email
,
940 author_date
= author_date
,
941 committer_name
= committer_name
,
942 committer_email
= committer_email
)
944 patch
.set_bottom(head
, backup
= True)
945 patch
.set_top(top
, backup
= True)
947 self
.log_patch(patch
, 'push(f)')
950 # stop the fast-forwarding, must do a real merge
954 unapplied
.remove(name
)
961 append_strings(self
.__applied
_file
, names
[0:forwarded
])
963 f
= file(self
.__unapplied
_file
, 'w+')
964 f
.writelines([line
+ '\n' for line
in unapplied
])
967 self
.__set
_current
(name
)
971 def merged_patches(self
, names
):
972 """Test which patches were merged upstream by reverse-applying
973 them in reverse order. The function returns the list of
974 patches detected to have been applied. The state of the tree
975 is restored to the original one
977 patches
= [Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
983 if git
.apply_diff(p
.get_top(), p
.get_bottom()):
984 merged
.append(p
.get_name())
991 def push_patch(self
, name
, empty
= False):
992 """Pushes a patch on the stack
994 unapplied
= self
.get_unapplied()
995 assert(name
in unapplied
)
997 self
.__begin
_stack
_check
()
999 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
1001 head
= git
.get_head()
1002 bottom
= patch
.get_bottom()
1003 top
= patch
.get_top()
1008 # top != bottom always since we have a commit for each patch
1010 # just make an empty patch (top = bottom = HEAD). This
1011 # option is useful to allow undoing already merged
1012 # patches. The top is updated by refresh_patch since we
1013 # need an empty commit
1014 patch
.set_bottom(head
, backup
= True)
1015 patch
.set_top(head
, backup
= True)
1017 elif head
== bottom
:
1018 # reset the backup information. No need for logging
1019 patch
.set_bottom(bottom
, backup
= True)
1020 patch
.set_top(top
, backup
= True)
1024 # new patch needs to be refreshed.
1025 # The current patch is empty after merge.
1026 patch
.set_bottom(head
, backup
= True)
1027 patch
.set_top(head
, backup
= True)
1029 # Try the fast applying first. If this fails, fall back to the
1031 if not git
.apply_diff(bottom
, top
):
1032 # if git.apply_diff() fails, the patch requires a diff3
1033 # merge and can be reported as modified
1036 # merge can fail but the patch needs to be pushed
1038 git
.merge(bottom
, head
, top
, recursive
= True)
1039 except git
.GitException
, ex
:
1040 print >> sys
.stderr
, \
1041 'The merge failed during "push". ' \
1042 'Use "refresh" after fixing the conflicts'
1044 append_string(self
.__applied
_file
, name
)
1046 unapplied
.remove(name
)
1047 f
= file(self
.__unapplied
_file
, 'w+')
1048 f
.writelines([line
+ '\n' for line
in unapplied
])
1051 self
.__set
_current
(name
)
1053 # head == bottom case doesn't need to refresh the patch
1054 if empty
or head
!= bottom
:
1056 # if the merge was OK and no conflicts, just refresh the patch
1057 # The GIT cache was already updated by the merge operation
1062 self
.refresh_patch(cache_update
= False, log
= log
)
1064 # we store the correctly merged files only for
1065 # tracking the conflict history. Note that the
1066 # git.merge() operations should always leave the index
1067 # in a valid state (i.e. only stage 0 files)
1068 self
.refresh_patch(cache_update
= False, log
= 'push(c)')
1069 raise StackException
, str(ex
)
1073 def undo_push(self
):
1074 name
= self
.get_current()
1077 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
1078 old_bottom
= patch
.get_old_bottom()
1079 old_top
= patch
.get_old_top()
1081 # the top of the patch is changed by a push operation only
1082 # together with the bottom (otherwise the top was probably
1083 # modified by 'refresh'). If they are both unchanged, there
1084 # was a fast forward
1085 if old_bottom
== patch
.get_bottom() and old_top
!= patch
.get_top():
1086 raise StackException
, 'No undo information available'
1089 self
.pop_patch(name
)
1090 ret
= patch
.restore_old_boundaries()
1092 self
.log_patch(patch
, 'undo')
1096 def pop_patch(self
, name
, keep
= False):
1097 """Pops the top patch from the stack
1099 applied
= self
.get_applied()
1101 assert(name
in applied
)
1103 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
1105 # only keep the local changes
1106 if keep
and not git
.apply_diff(git
.get_head(), patch
.get_bottom()):
1107 raise StackException
, \
1108 'Failed to pop patches while preserving the local changes'
1110 git
.switch(patch
.get_bottom(), keep
)
1112 # save the new applied list
1113 idx
= applied
.index(name
) + 1
1115 popped
= applied
[:idx
]
1117 unapplied
= popped
+ self
.get_unapplied()
1119 f
= file(self
.__unapplied
_file
, 'w+')
1120 f
.writelines([line
+ '\n' for line
in unapplied
])
1126 f
= file(self
.__applied
_file
, 'w+')
1127 f
.writelines([line
+ '\n' for line
in applied
])
1131 self
.__set
_current
(None)
1133 self
.__set
_current
(applied
[-1])
1135 self
.__end
_stack
_check
()
1137 def empty_patch(self
, name
):
1138 """Returns True if the patch is empty
1140 self
.__patch
_name
_valid
(name
)
1141 patch
= Patch(name
, self
.__patch
_dir
, self
.__refs
_dir
)
1142 bottom
= patch
.get_bottom()
1143 top
= patch
.get_top()
1147 elif git
.get_commit(top
).get_tree() \
1148 == git
.get_commit(bottom
).get_tree():
1153 def rename_patch(self
, oldname
, newname
):
1154 self
.__patch
_name
_valid
(newname
)
1156 applied
= self
.get_applied()
1157 unapplied
= self
.get_unapplied()
1159 if oldname
== newname
:
1160 raise StackException
, '"To" name and "from" name are the same'
1162 if newname
in applied
or newname
in unapplied
:
1163 raise StackException
, 'Patch "%s" already exists' % newname
1165 if self
.patch_hidden(oldname
):
1166 self
.unhide_patch(oldname
)
1167 self
.hide_patch(newname
)
1169 if oldname
in unapplied
:
1170 Patch(oldname
, self
.__patch
_dir
, self
.__refs
_dir
).rename(newname
)
1171 unapplied
[unapplied
.index(oldname
)] = newname
1173 f
= file(self
.__unapplied
_file
, 'w+')
1174 f
.writelines([line
+ '\n' for line
in unapplied
])
1176 elif oldname
in applied
:
1177 Patch(oldname
, self
.__patch
_dir
, self
.__refs
_dir
).rename(newname
)
1178 if oldname
== self
.get_current():
1179 self
.__set
_current
(newname
)
1181 applied
[applied
.index(oldname
)] = newname
1183 f
= file(self
.__applied
_file
, 'w+')
1184 f
.writelines([line
+ '\n' for line
in applied
])
1187 raise StackException
, 'Unknown patch "%s"' % oldname
1189 def log_patch(self
, patch
, message
):
1190 """Generate a log commit for a patch
1192 top
= git
.get_commit(patch
.get_top())
1193 msg
= '%s\t%s' % (message
, top
.get_id_hash())
1195 old_log
= patch
.get_log()
1201 log
= git
.commit(message
= msg
, parents
= parents
,
1202 cache_update
= False, tree_id
= top
.get_tree(),
1206 def hide_patch(self
, name
):
1207 """Add the patch to the hidden list.
1209 if not self
.patch_exists(name
):
1210 raise StackException
, 'Unknown patch "%s"' % name
1211 elif self
.patch_hidden(name
):
1212 raise StackException
, 'Patch "%s" already hidden' % name
1214 append_string(self
.__hidden
_file
, name
)
1216 def unhide_patch(self
, name
):
1217 """Add the patch to the hidden list.
1219 if not self
.patch_exists(name
):
1220 raise StackException
, 'Unknown patch "%s"' % name
1221 hidden
= self
.get_hidden()
1222 if not name
in hidden
:
1223 raise StackException
, 'Patch "%s" not hidden' % name
1227 f
= file(self
.__hidden
_file
, 'w+')
1228 f
.writelines([line
+ '\n' for line
in hidden
])