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