chiark / gitweb /
Merge branch 'stable'
[stgit] / stgit / commands / common.py
index d8323a823daa4962e33261cc4530a4ab38410dff..15fdde2517a7248a2cb3bc86647b34ef74c5e53d 100644 (file)
@@ -19,117 +19,87 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 import sys, os, os.path, re
 """
 
 import sys, os, os.path, re
-from optparse import OptionParser, make_option
-
+from stgit.exception import *
 from stgit.utils import *
 from stgit.out import *
 from stgit.utils import *
 from stgit.out import *
+from stgit.run import *
 from stgit import stack, git, basedir
 from stgit.config import config, file_extensions
 from stgit import stack, git, basedir
 from stgit.config import config, file_extensions
-from stgit.run import *
-
-crt_series = None
-
+from stgit.lib import stack as libstack
+from stgit.lib import git as libgit
+from stgit.lib import log
 
 # Command exception class
 
 # Command exception class
-class CmdException(Exception):
+class CmdException(StgException):
     pass
 
     pass
 
-class CmdRunException(CmdException):
-    pass
-class CmdRun(Run):
-    exc = CmdRunException
-
 # Utility functions
 # Utility functions
-class RevParseException(Exception):
-    """Revision spec parse error."""
-    pass
-
 def parse_rev(rev):
 def parse_rev(rev):
-    """Parse a revision specification into its
-    patchname@branchname//patch_id parts. If no branch name has a slash
-    in it, also accept / instead of //."""
-    if '/' in ''.join(git.get_heads()):
-        # We have branch names with / in them.
-        branch_chars = r'[^@]'
-        patch_id_mark = r'//'
-    else:
-        # No / in branch names.
-        branch_chars = r'[^@/]'
-        patch_id_mark = r'(/|//)'
-    patch_re = r'(?P<patch>[^@/]+)'
-    branch_re = r'@(?P<branch>%s+)' % branch_chars
-    patch_id_re = r'%s(?P<patch_id>[a-z.]*)' % patch_id_mark
-
-    # Try //patch_id.
-    m = re.match(r'^%s$' % patch_id_re, rev)
-    if m:
-        return None, None, m.group('patch_id')
-
-    # Try path[@branch]//patch_id.
-    m = re.match(r'^%s(%s)?%s$' % (patch_re, branch_re, patch_id_re), rev)
-    if m:
-        return m.group('patch'), m.group('branch'), m.group('patch_id')
-
-    # Try patch[@branch].
-    m = re.match(r'^%s(%s)?$' % (patch_re, branch_re), rev)
-    if m:
-        return m.group('patch'), m.group('branch'), None
-
-    # No, we can't parse that.
-    raise RevParseException
-
-def git_id(rev):
+    """Parse a revision specification into its branch:patch parts.
+    """
+    try:
+        branch, patch = rev.split(':', 1)
+    except ValueError:
+        branch = None
+        patch = rev
+
+    return (branch, patch)
+
+def git_id(crt_series, rev):
     """Return the GIT id
     """
     """Return the GIT id
     """
-    if not rev:
-        return None
+    # TODO: remove this function once all the occurrences were converted
+    # to git_commit()
+    repository = libstack.Repository.default()
+    return git_commit(rev, repository, crt_series.get_name()).sha1
+
+def git_commit(name, repository, branch_name = None):
+    """Return the a Commit object if 'name' is a patch name or Git commit.
+    The patch names allowed are in the form '<branch>:<patch>' and can
+    be followed by standard symbols used by git rev-parse. If <patch>
+    is '{base}', it represents the bottom of the stack.
+    """
+    # Try a [branch:]patch name first
+    branch, patch = parse_rev(name)
+    if not branch:
+        branch = branch_name or repository.current_branch_name
+
+    # The stack base
+    if patch.startswith('{base}'):
+        base_id = repository.get_stack(branch).base.sha1
+        return repository.rev_parse(base_id +
+                                    strip_prefix('{base}', patch))
+
+    # Other combination of branch and patch
     try:
     try:
-        patch, branch, patch_id = parse_rev(rev)
-        if branch == None:
-            series = crt_series
-        else:
-            series = stack.Series(branch)
-        if patch == None:
-            patch = series.get_current()
-            if not patch:
-                raise CmdException, 'No patches applied'
-        if patch in series.get_applied() or patch in series.get_unapplied() or \
-               patch in series.get_hidden():
-            if patch_id in ['top', '', None]:
-                return series.get_patch(patch).get_top()
-            elif patch_id == 'bottom':
-                return series.get_patch(patch).get_bottom()
-            elif patch_id == 'top.old':
-                return series.get_patch(patch).get_old_top()
-            elif patch_id == 'bottom.old':
-                return series.get_patch(patch).get_old_bottom()
-            elif patch_id == 'log':
-                return series.get_patch(patch).get_log()
-        if patch == 'base' and patch_id == None:
-            return series.get_base()
-    except RevParseException:
+        return repository.rev_parse('patches/%s/%s' % (branch, patch),
+                                    discard_stderr = True)
+    except libgit.RepositoryException:
         pass
         pass
-    return git.rev_parse(rev + '^{commit}')
+
+    # Try a Git commit
+    try:
+        return repository.rev_parse(name, discard_stderr = True)
+    except libgit.RepositoryException:
+        raise CmdException('%s: Unknown patch or revision name' % name)
 
 def check_local_changes():
     if git.local_changes():
 
 def check_local_changes():
     if git.local_changes():
-        raise CmdException, \
-              'local changes in the tree. Use "refresh" or "status --reset"'
+        raise CmdException('local changes in the tree. Use "refresh" or'
+                           ' "status --reset"')
 
 
-def check_head_top_equal():
+def check_head_top_equal(crt_series):
     if not crt_series.head_top_equal():
     if not crt_series.head_top_equal():
-        raise CmdException(
-            'HEAD and top are not the same. You probably committed\n'
-            '  changes to the tree outside of StGIT. To bring them\n'
-            '  into StGIT, use the "assimilate" command')
+        raise CmdException('HEAD and top are not the same. This can happen'
+                           ' if you modify a branch with git. "stg repair'
+                           ' --help" explains more about what to do next.')
 
 def check_conflicts():
 
 def check_conflicts():
-    if os.path.exists(os.path.join(basedir.get(), 'conflicts')):
-        raise CmdException, \
-              'Unsolved conflicts. Please resolve them first or\n' \
-              '  revert the changes with "status --reset"'
+    if git.get_conflicts():
+        raise CmdException('Unsolved conflicts. Please resolve them first'
+                           ' or revert the changes with "status --reset"')
 
 
-def print_crt_patch(branch = None):
+def print_crt_patch(crt_series, branch = None):
     if not branch:
         patch = crt_series.get_current()
     else:
     if not branch:
         patch = crt_series.get_current()
     else:
@@ -140,29 +110,11 @@ def print_crt_patch(branch = None):
     else:
         out.info('No patches applied')
 
     else:
         out.info('No patches applied')
 
-def resolved(filename, reset = None):
-    if reset:
-        reset_file = filename + file_extensions()[reset]
-        if os.path.isfile(reset_file):
-            if os.path.isfile(filename):
-                os.remove(filename)
-            os.rename(reset_file, filename)
-
-    git.update_cache([filename], force = True)
-
-    for ext in file_extensions().values():
-        fn = filename + ext
-        if os.path.isfile(fn):
-            os.remove(fn)
-
 def resolved_all(reset = None):
     conflicts = git.get_conflicts()
 def resolved_all(reset = None):
     conflicts = git.get_conflicts()
-    if conflicts:
-        for filename in conflicts:
-            resolved(filename, reset)
-        os.remove(os.path.join(basedir.get(), 'conflicts'))
+    git.resolved(conflicts, reset)
 
 
-def push_patches(patches, check_merged = False):
+def push_patches(crt_series, patches, check_merged = False):
     """Push multiple patches onto the stack. This function is shared
     between the push and pull commands
     """
     """Push multiple patches onto the stack. This function is shared
     between the push and pull commands
     """
@@ -201,7 +153,7 @@ def push_patches(patches, check_merged = False):
             else:
                 out.done()
 
             else:
                 out.done()
 
-def pop_patches(patches, keep = False):
+def pop_patches(crt_series, patches, keep = False):
     """Pop the patches in the list from the stack. It is assumed that
     the patches are listed in the stack reverse order.
     """
     """Pop the patches in the list from the stack. It is assumed that
     the patches are listed in the stack reverse order.
     """
@@ -221,6 +173,8 @@ def parse_patches(patch_args, patch_list, boundary = 0, ordered = False):
     a list. The names can be individual patches and/or in the
     patch1..patch2 format.
     """
     a list. The names can be individual patches and/or in the
     patch1..patch2 format.
     """
+    # in case it receives a tuple
+    patch_list = list(patch_list)
     patches = []
 
     for name in patch_args:
     patches = []
 
     for name in patch_args:
@@ -279,29 +233,19 @@ def parse_patches(patch_args, patch_list, boundary = 0, ordered = False):
     return patches
 
 def name_email(address):
     return patches
 
 def name_email(address):
-    """Return a tuple consisting of the name and email parsed from a
-    standard 'name <email>' or 'email (name)' string
-    """
-    address = re.sub('[\\\\"]', '\\\\\g<0>', address)
-    str_list = re.findall('^(.*)\s*<(.*)>\s*$', address)
-    if not str_list:
-        str_list = re.findall('^(.*)\s*\((.*)\)\s*$', address)
-        if not str_list:
-            raise CmdException, 'Incorrect "name <email>"/"email (name)" string: %s' % address
-        return ( str_list[0][1], str_list[0][0] )
-
-    return str_list[0]
+    p = parse_name_email(address)
+    if p:
+        return p
+    else:
+        raise CmdException('Incorrect "name <email>"/"email (name)" string: %s'
+                           % address)
 
 def name_email_date(address):
 
 def name_email_date(address):
-    """Return a tuple consisting of the name, email and date parsed
-    from a 'name <email> date' string
-    """
-    address = re.sub('[\\\\"]', '\\\\\g<0>', address)
-    str_list = re.findall('^(.*)\s*<(.*)>\s*(.*)\s*$', address)
-    if not str_list:
-        raise CmdException, 'Incorrect "name <email> date" string: %s' % address
-
-    return str_list[0]
+    p = parse_name_email_date(address)
+    if p:
+        return p
+    else:
+        raise CmdException('Incorrect "name <email> date" string: %s' % address)
 
 def address_or_alias(addr_str):
     """Return the address if it contains an e-mail address or look up
 
 def address_or_alias(addr_str):
     """Return the address if it contains an e-mail address or look up
@@ -323,14 +267,7 @@ def address_or_alias(addr_str):
                  for addr in addr_str.split(',')]
     return ', '.join([addr for addr in addr_list if addr])
 
                  for addr in addr_str.split(',')]
     return ', '.join([addr for addr in addr_list if addr])
 
-def prepare_rebase(force=None):
-    if not force:
-        # Be sure we won't loose results of stg-(un)commit by error.
-        # Do not require an existing orig-base for compatibility with 0.12 and earlier.
-        origbase = crt_series._get_field('orig-base')
-        if origbase and crt_series.get_base() != origbase:
-            raise CmdException, 'Rebasing would possibly lose data'
-
+def prepare_rebase(crt_series):
     # pop all patches
     applied = crt_series.get_applied()
     if len(applied) > 0:
     # pop all patches
     applied = crt_series.get_applied()
     if len(applied) > 0:
@@ -339,32 +276,29 @@ def prepare_rebase(force=None):
         out.done()
     return applied
 
         out.done()
     return applied
 
-def rebase(target):
-    if target == git.get_head():
+def rebase(crt_series, target):
+    try:
+        tree_id = git_id(crt_series, target)
+    except:
+        # it might be that we use a custom rebase command with its own
+        # target type
+        tree_id = target
+    if tree_id == git.get_head():
         out.info('Already at "%s", no need for rebasing.' % target)
         return
         out.info('Already at "%s", no need for rebasing.' % target)
         return
-    command = config.get('branch.%s.stgit.rebasecmd' % git.get_head_file()) \
-                or config.get('stgit.rebasecmd')
     if target:
     if target:
-        args = [target]
         out.start('Rebasing to "%s"' % target)
         out.start('Rebasing to "%s"' % target)
-    elif command:
-        args = []
-        out.start('Rebasing to the default target')
     else:
     else:
-        raise CmdException, 'Default rebasing requires a target'
-    if command:
-        CmdRun(*(command.split() + args)).run()
-    else:
-        git.reset(tree_id = git_id(target))
+        out.start('Rebasing to the default target')
+    git.rebase(tree_id = tree_id)
     out.done()
 
     out.done()
 
-def post_rebase(applied, nopush, merged):
+def post_rebase(crt_series, applied, nopush, merged):
     # memorize that we rebased to here
     crt_series._set_field('orig-base', git.get_head())
     # push the patches back
     if not nopush:
     # memorize that we rebased to here
     crt_series._set_field('orig-base', git.get_head())
     # push the patches back
     if not nopush:
-        push_patches(applied, merged)
+        push_patches(crt_series, applied, merged)
 
 #
 # Patch description/e-mail/diff parsing
 
 #
 # Patch description/e-mail/diff parsing
@@ -485,13 +419,99 @@ def parse_mail(msg):
 
     return (descr, authname, authemail, authdate, diff)
 
 
     return (descr, authname, authemail, authdate, diff)
 
-def parse_patch(fobj):
-    """Parse the input file and return (description, authname,
+def parse_patch(text, contains_diff):
+    """Parse the input text and return (description, authname,
     authemail, authdate, diff)
     """
     authemail, authdate, diff)
     """
-    descr, diff = __split_descr_diff(fobj.read())
-    descr, authname, authemail, authdate = __parse_description(descr)
+    if contains_diff:
+        (text, diff) = __split_descr_diff(text)
+    else:
+        diff = None
+    (descr, authname, authemail, authdate) = __parse_description(text)
 
     # we don't yet have an agreed place for the creation date.
     # Just return None
     return (descr, authname, authemail, authdate, diff)
 
     # we don't yet have an agreed place for the creation date.
     # Just return None
     return (descr, authname, authemail, authdate, diff)
+
+def readonly_constant_property(f):
+    """Decorator that converts a function that computes a value to an
+    attribute that returns the value. The value is computed only once,
+    the first time it is accessed."""
+    def new_f(self):
+        n = '__' + f.__name__
+        if not hasattr(self, n):
+            setattr(self, n, f(self))
+        return getattr(self, n)
+    return property(new_f)
+
+class DirectoryException(StgException):
+    pass
+
+class _Directory(object):
+    def __init__(self, needs_current_series = True, log = True):
+        self.needs_current_series =  needs_current_series
+        self.log = log
+    @readonly_constant_property
+    def git_dir(self):
+        try:
+            return Run('git', 'rev-parse', '--git-dir'
+                       ).discard_stderr().output_one_line()
+        except RunException:
+            raise DirectoryException('No git repository found')
+    @readonly_constant_property
+    def __topdir_path(self):
+        try:
+            lines = Run('git', 'rev-parse', '--show-cdup'
+                        ).discard_stderr().output_lines()
+            if len(lines) == 0:
+                return '.'
+            elif len(lines) == 1:
+                return lines[0]
+            else:
+                raise RunException('Too much output')
+        except RunException:
+            raise DirectoryException('No git repository found')
+    @readonly_constant_property
+    def is_inside_git_dir(self):
+        return { 'true': True, 'false': False
+                 }[Run('git', 'rev-parse', '--is-inside-git-dir'
+                       ).output_one_line()]
+    @readonly_constant_property
+    def is_inside_worktree(self):
+        return { 'true': True, 'false': False
+                 }[Run('git', 'rev-parse', '--is-inside-work-tree'
+                       ).output_one_line()]
+    def cd_to_topdir(self):
+        os.chdir(self.__topdir_path)
+    def write_log(self, msg):
+        if self.log:
+            log.compat_log_entry(msg)
+
+class DirectoryAnywhere(_Directory):
+    def setup(self):
+        pass
+
+class DirectoryHasRepository(_Directory):
+    def setup(self):
+        self.git_dir # might throw an exception
+        log.compat_log_external_mods()
+
+class DirectoryInWorktree(DirectoryHasRepository):
+    def setup(self):
+        DirectoryHasRepository.setup(self)
+        if not self.is_inside_worktree:
+            raise DirectoryException('Not inside a git worktree')
+
+class DirectoryGotoToplevel(DirectoryInWorktree):
+    def setup(self):
+        DirectoryInWorktree.setup(self)
+        self.cd_to_topdir()
+
+class DirectoryHasRepositoryLib(_Directory):
+    """For commands that use the new infrastructure in stgit.lib.*."""
+    def __init__(self):
+        self.needs_current_series = False
+        self.log = False # stgit.lib.transaction handles logging
+    def setup(self):
+        # This will throw an exception if we don't have a repository.
+        self.repository = libstack.Repository.default()