Remove the resolved command
[stgit] / stgit / commands / common.py
index 9815400..bc8266e 100644 (file)
@@ -18,113 +18,112 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-import sys, os, os.path, re
-from optparse import OptionParser, make_option
-
+import sys, os, os.path, re, email.Utils
 from stgit.exception import *
 from stgit.utils import *
 from stgit.out import *
 from stgit.exception 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
-
-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
 class CmdException(StgException):
     pass
 
 # Utility functions
 
 # Command exception class
 class CmdException(StgException):
     pass
 
 # Utility functions
-class RevParseException(StgException):
-    """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 get_public_ref(branch_name):
+    """Return the public ref of the branch."""
+    public_ref = config.get('branch.%s.public' % branch_name)
+    if not public_ref:
+        public_ref = 'refs/heads/%s.public' % branch_name
+    return public_ref
+
+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. If <patch> is
+    {public}, it represents the public branch corresponding to the stack as
+    described in the 'publish' command.
+    """
+    # 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))
+    elif patch.startswith('{public}'):
+        public_ref = get_public_ref(branch)
+        return repository.rev_parse(public_ref +
+                                    strip_prefix('{public}', patch),
+                                    discard_stderr = True)
+
+    # 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 color_diff_flags():
+    """Return the git flags for coloured diff output if the configuration and
+    stdout allows."""
+    stdout_is_tty = (sys.stdout.isatty() and 'true') or 'false'
+    if config.get_colorbool('color.diff', stdout_is_tty) == 'true':
+        return ['--color']
+    else:
+        return []
 
 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. This can happen if you
-   modify a branch with git. The "assimilate" command can
-   fix this situation.""")
+        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 fix the conflicts'
+                           ' then use "git add --update <files>" 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:
@@ -135,29 +134,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
     """
@@ -196,7 +177,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.
     """
@@ -216,6 +197,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:
@@ -274,51 +257,35 @@ 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 = email.Utils.parseaddr(address)
+    if p[1]:
+        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_pair):
+    """Return a name-email tuple the e-mail address is valid or look up
     the aliases in the config files.
     """
     the aliases in the config files.
     """
-    def __address_or_alias(addr):
-        if not addr:
-            return None
-        if addr.find('@') >= 0:
-            # it's an e-mail address
-            return addr
-        alias = config.get('mail.alias.'+addr)
-        if alias:
-            # it's an alias
-            return alias
-        raise CmdException, 'unknown e-mail alias: %s' % addr
-
-    addr_list = [__address_or_alias(addr.strip())
-                 for addr in addr_str.split(',')]
-    return ', '.join([addr for addr in addr_list if addr])
-
-def prepare_rebase():
+    addr = addr_pair[1]
+    if '@' in addr:
+        # it's an e-mail address
+        return addr_pair
+    alias = config.get('mail.alias.' + addr)
+    if alias:
+        # it's an alias
+        return name_email(alias)
+    raise CmdException, 'unknown e-mail alias: %s' % addr
+
+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:
@@ -327,9 +294,9 @@ def prepare_rebase():
         out.done()
     return applied
 
         out.done()
     return applied
 
-def rebase(target):
+def rebase(crt_series, target):
     try:
     try:
-        tree_id = git_id(target)
+        tree_id = git_id(crt_series, target)
     except:
         # it might be that we use a custom rebase command with its own
         # target type
     except:
         # it might be that we use a custom rebase command with its own
         # target type
@@ -344,12 +311,12 @@ def rebase(target):
     git.rebase(tree_id = tree_id)
     out.done()
 
     git.rebase(tree_id = tree_id)
     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
@@ -470,13 +437,125 @@ 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)
+
+def update_commit_data(cd, options, allow_edit = False):
+    """Return a new CommitData object updated according to the command line
+    options."""
+    # Set the commit message from commandline.
+    if options.message != None:
+        cd = cd.set_message(options.message)
+
+    # Modify author data.
+    cd = cd.set_author(options.author(cd.author))
+
+    # Add Signed-off-by: or similar.
+    if options.sign_str != None:
+        sign_str = options.sign_str
+    else:
+        sign_str = config.get("stgit.autosign")
+    if sign_str != None:
+        cd = cd.set_message(
+            add_sign_line(cd.message, sign_str,
+                          cd.committer.name, cd.committer.email))
+
+    # Let user edit the commit message manually.
+    if allow_edit and not options.message:
+        cd = cd.set_message(edit_string(cd.message, '.stgit-new.txt'))
+
+    return cd
+
+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()