Add --sign and --ack options to "stg import"
[stgit.git] / stgit / stack.py
blobb19ff4d059c7d373e6478ca338a05ab2747d411f
1 """Basic quilt-like functionality
2 """
4 __copyright__ = """
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
19 """
21 import sys, os, re
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):
32 pass
34 class FilterUntil:
35 def __init__(self):
36 self.should_print = True
37 def __call__(self, x, until_test, prefix):
38 if until_test(x):
39 self.should_print = False
40 if self.should_print:
41 return x[0:len(prefix)] != prefix
42 return False
45 # Functions
47 __comment_prefix = 'STG:'
48 __patch_prefix = 'STG_PATCH:'
50 def __clean_comments(f):
51 """Removes lines marked for status in a commit file
52 """
53 f.seek(0)
55 # remove status-prefixed lines
56 lines = f.readlines()
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':
64 del lines[-1]
66 f.seek(0); f.truncate()
67 f.writelines(lines)
69 def edit_file(series, line, comment, show_patch = True):
70 fname = '.stgitmsg.txt'
71 tmpl = templates.get_template('patchdescr.tmpl')
73 f = file(fname, 'w+')
74 if line:
75 print >> f, line
76 elif tmpl:
77 print >> f, tmpl,
78 else:
79 print >> f
80 print >> f, __comment_prefix, comment
81 print >> f, __comment_prefix, \
82 'Lines prefixed with "%s" will be automatically removed.' \
83 % __comment_prefix
84 print >> f, __comment_prefix, \
85 'Trailing empty lines will be automatically removed.'
87 if show_patch:
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())
91 f.write(diff_str)
93 #Vim modeline must be near the end.
94 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
95 f.close()
97 call_editor(fname)
99 f = file(fname, 'r+')
101 __clean_comments(f)
102 f.seek(0)
103 result = f.read()
105 f.close()
106 os.remove(fname)
108 return result
111 # Classes
114 class StgitObject:
115 """An object with stgit-like properties stored as files in a directory
117 def _set_dir(self, dir):
118 self.__dir = dir
119 def _dir(self):
120 return 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)
129 if line == '':
130 return None
131 else:
132 return line
133 else:
134 return None
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):
141 os.remove(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
153 self.__name = name
154 self._set_dir(os.path.join(self.__series_dir, self.__name))
155 self.__refs_base = refs_base
156 self.__init_refs()
158 def create(self):
159 os.mkdir(self._dir())
160 self.create_empty_field('bottom')
161 self.create_empty_field('top')
163 def delete(self):
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)
171 def get_name(self):
172 return self.__name
174 def rename(self, newname):
175 olddir = self._dir()
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))
180 self.__init_refs()
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):
194 top = self.get_top()
195 if top:
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):
205 if backup:
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')
213 def get_top(self):
214 return self._get_field('top')
216 def set_top(self, value, backup = False):
217 if backup:
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')
227 if top and bottom:
228 self._set_field('bottom', bottom)
229 self._set_field('top', top)
230 self.__update_top_ref(top)
231 return True
232 else:
233 return False
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)
271 def get_log(self):
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.
279 FORMAT_VERSION = 2
281 class PatchSet(StgitObject):
282 def __init__(self, name = None):
283 try:
284 if name:
285 self.set_name (name)
286 else:
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()))
294 def get_name(self):
295 return self.__name
296 def set_name(self, name):
297 self.__name = name
299 def _basedir(self):
300 return self.__base_dir
302 def get_head(self):
303 """Return the head of the branch
305 crt = self.get_current_patch()
306 if crt:
307 return crt.get_top()
308 else:
309 return self.get_base()
311 def get_protected(self):
312 return os.path.isfile(os.path.join(self._dir(), 'protected'))
314 def protect(self):
315 protect_file = os.path.join(self._dir(), 'protected')
316 if not os.path.isfile(protect_file):
317 create_empty_file(protect_file)
319 def unprotect(self):
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):
331 if line:
332 config.set(self.__branch_descr(), line)
333 else:
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()
340 if not crt:
341 # we don't care, no patches applied
342 return True
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')
372 # trash directory
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())
390 if fv:
391 # Great, there's an explicitly recorded format version
392 # number, which means that the branch is initialized and
393 # of that exact version.
394 return int(fv)
395 elif ofv:
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())
399 return int(ofv)
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.
403 return 1
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.
407 return 0
408 else:
409 # The branch doesn't seem to be initialized at all.
410 return None
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)
414 def mkdir(d):
415 if not os.path.isdir(d):
416 os.makedirs(d)
417 def rm(f):
418 if os.path.exists(f):
419 os.remove(f)
420 def rm_ref(ref):
421 if git.ref_exists(ref):
422 git.delete_ref(ref)
424 # Update 0 -> 1.
425 if get_format_version() == 0:
426 mkdir(os.path.join(branch_dir, 'trash'))
427 patch_dir = os.path.join(branch_dir, 'patches')
428 mkdir(patch_dir)
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)
438 # Update 1 -> 2.
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)
443 if desc:
444 config.set('branch.%s.description' % self.get_name(), desc)
445 rm(desc_file)
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()
470 if not crt:
471 return None
472 return self.get_patch(crt)
474 def get_current(self):
475 """Return the name of the topmost patch, or None if there is
476 no such patch."""
477 try:
478 applied = self.get_applied()
479 except StackException:
480 # No "applied" file: branch is not initialized.
481 return None
482 try:
483 return applied[-1]
484 except IndexError:
485 # No patches applied.
486 return None
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):
500 return []
501 return read_strings(self.__hidden_file)
503 def get_base(self):
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()
507 if bottommost:
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())
514 if value:
515 return value
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())))
522 return 'origin'
523 else:
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())
531 if value:
532 return value
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'
539 else:
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):
550 if localbranch:
551 if remote:
552 self.__set_parent_remote(remote)
553 self.__set_parent_branch(localbranch)
554 # We'll enforce this later
555 # else:
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
578 otherwise."""
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):
605 """Renames a series
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):
632 """Clones a series
634 try:
635 # allow cloning of branches not under StGIT control
636 base = self.get_base()
637 except:
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
646 try:
647 # allow cloning of branches not under StGIT control
648 applied = self.get_applied()
649 unapplied = self.get_unapplied()
650 patches = applied + unapplied
651 patches.reverse()
652 except:
653 patches = applied = unapplied = []
654 for p in patches:
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())
663 if patch.get_log():
664 out.info('Setting log to %s' % patch.get_log())
665 newpatch.set_log(patch.get_log())
666 else:
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())
674 if value:
675 config.set('branch.%s.remote' % target_series, value)
677 value = config.get('branch.%s.merge' % self.get_name())
678 if value:
679 config.set('branch.%s.merge' % target_series, value)
681 value = config.get('branch.%s.stgit.parentbranch' % self.get_name())
682 if value:
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'
693 for p in 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)
715 else:
716 out.warn('Patch directory %s is not empty' % self.__patch_dir)
718 try:
719 os.removedirs(self._dir())
720 except OSError:
721 raise StackException('Series directory %s is not empty'
722 % self._dir())
724 try:
725 git.delete_branch(self.get_name())
726 except GitException:
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,
738 show_patch = False,
739 cache_update = True,
740 author_name = None, author_email = None,
741 author_date = None,
742 committer_name = None, committer_email = None,
743 backup = False, sign_str = None, log = 'refresh',
744 notes = None):
745 """Generates a new commit for the given patch
747 name = self.get_current()
748 if not name:
749 raise StackException, 'No patches applied'
751 patch = self.get_patch(name)
753 descr = patch.get_description()
754 if not (message or descr):
755 edit = True
756 descr = ''
757 elif message:
758 descr = message
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)
765 if not author_name:
766 author_name = patch.get_authname()
767 if not author_email:
768 author_email = patch.get_authemail()
769 if not author_date:
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,
783 allowempty = True,
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)
799 if log:
800 self.log_patch(patch, log, notes)
802 return commit_id
804 def undo_refresh(self):
805 """Undo the patch boundaries changes caused by 'refresh'
807 name = self.get_current()
808 assert(name)
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
833 if name != None:
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:
839 descr = edit_file(
840 self, None,
841 'Please enter the description for the patch above.',
842 show_patch)
843 else:
844 descr = message
846 head = git.get_head()
848 if name == None:
849 name = make_patch_name(descr, self.patch_exists)
851 patch = self.get_patch(name)
852 patch.create()
854 if not bottom:
855 bottom = head
856 if not top:
857 top = head
859 patch.set_bottom(bottom)
860 patch.set_top(top)
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)
868 if before_existing:
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')
872 commit = False
873 elif unapplied:
874 patches = [patch.get_name()] + self.get_unapplied()
875 write_strings(self.__unapplied_file, patches)
876 set_head = False
877 else:
878 append_string(self.__applied_file, patch.get_name())
879 set_head = True
881 if commit:
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')
900 return patch
902 def delete_patch(self, name):
903 """Deletes a patch
905 self.__patch_name_valid(name)
906 patch = self.get_patch(name)
908 if self.__patch_is_current(patch):
909 self.pop_patch(name)
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())
919 patch.delete()
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()
933 forwarded = 0
934 top = git.get_head()
936 for name in names:
937 assert(name in unapplied)
939 patch = self.get_patch(name)
941 head = top
942 bottom = patch.get_bottom()
943 top = patch.get_top()
945 # top != bottom always since we have a commit for each patch
946 if head == bottom:
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)
952 else:
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
957 # for it
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,
969 tree_id = top_tree,
970 allowempty = True,
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)')
981 else:
982 top = head
983 # stop the fast-forwarding, must do a real merge
984 break
986 forwarded+=1
987 unapplied.remove(name)
989 if forwarded == 0:
990 return 0
992 git.switch(top)
994 append_strings(self.__applied_file, names[0:forwarded])
995 write_strings(self.__unapplied_file, unapplied)
997 return forwarded
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]
1006 patches.reverse()
1008 merged = []
1009 for p in patches:
1010 if git.apply_diff(p.get_top(), p.get_bottom()):
1011 merged.append(p.get_name())
1012 merged.reverse()
1014 git.reset()
1016 return merged
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()
1030 ex = None
1031 modified = False
1033 # top != bottom always since we have a commit for each patch
1034 if empty:
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)
1041 modified = 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)
1047 git.switch(top)
1048 else:
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
1055 # three-way merge
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
1059 modified = True
1061 # merge can fail but the patch needs to be pushed
1062 try:
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:
1076 if not ex:
1077 # if the merge was OK and no conflicts, just refresh the patch
1078 # The GIT cache was already updated by the merge operation
1079 if modified:
1080 log = 'push(m)'
1081 else:
1082 log = 'push'
1083 self.refresh_patch(cache_update = False, log = log)
1084 else:
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)
1092 return modified
1094 def undo_push(self):
1095 name = self.get_current()
1096 assert(name)
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'
1109 git.reset()
1110 self.pop_patch(name)
1111 ret = patch.restore_old_boundaries()
1112 if ret:
1113 self.log_patch(patch, 'undo')
1115 return ret
1117 def pop_patch(self, name, keep = False):
1118 """Pops the top patch from the stack
1120 applied = self.get_applied()
1121 applied.reverse()
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)
1131 else:
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]
1138 popped.reverse()
1139 unapplied = popped + self.get_unapplied()
1140 write_strings(self.__unapplied_file, unapplied)
1142 del applied[:idx]
1143 applied.reverse()
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()
1154 if bottom == top:
1155 return True
1156 elif git.get_commit(top).get_tree() \
1157 == git.get_commit(bottom).get_tree():
1158 return True
1160 return False
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)
1183 else:
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()
1192 if message is None:
1193 # replace the current log entry
1194 if not old_log:
1195 raise StackException, \
1196 'No log entry to annotate for patch "%s"' \
1197 % patch.get_name()
1198 replace = True
1199 log_commit = git.get_commit(old_log)
1200 msg = log_commit.get_log().split('\n')[0]
1201 log_parent = log_commit.get_parent()
1202 if log_parent:
1203 parents = [log_parent]
1204 else:
1205 parents = []
1206 else:
1207 # generate a new log entry
1208 replace = False
1209 msg = '%s\t%s' % (message, top.get_id_hash())
1210 if old_log:
1211 parents = [old_log]
1212 else:
1213 parents = []
1215 if notes:
1216 msg += '\n\n' + notes
1218 log = git.commit(message = msg, parents = parents,
1219 cache_update = False, tree_id = top.get_tree(),
1220 allowempty = True)
1221 patch.set_log(log)
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
1234 else:
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
1252 else:
1253 raise StackException, 'Unknown patch "%s"' % name
1255 hidden.remove(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)