.. versionadded:: 2015.8.0

    Returns information on worktrees

    .. versionchanged:: 2015.8.4
        Version 2.7.0 added the ``list`` subcommand to `git-worktree(1)`_ which
        provides a lot of additional information. The return data has been
        changed to include this information, even for pre-2.7.0 versions of
        git. In addition, if a worktree has a detached head, then any tags
        which point to the worktree's HEAD will be included in the return data.

    .. note::
        By default, only worktrees for which the worktree directory is still
        present are returned, but this can be changed using the ``all`` and
        ``stale`` arguments (described below).

    cwd
        The path to the git checkout

    user
        User under which to run the git command. By default, the command is run
        by the user under which the minion is running.

    password
        Windows only. Required when specifying ``user``. This parameter will be
        ignored on non-Windows platforms.

      .. versionadded:: 2016.3.4

    all : False
        If ``True``, then return all worktrees tracked under
        $GIT_DIR/worktrees, including ones for which the gitdir is no longer
        present.

    stale : False
        If ``True``, return *only* worktrees whose gitdir is no longer present.

    .. note::
        Only one of ``all`` and ``stale`` can be set to ``True``.

    output_encoding
        Use this option to specify which encoding to use to decode the output
        from any git commands which are run. This should not be needed in most
        cases.

        .. note::
            This should only be needed if the files in the repository were
            created with filenames using an encoding other than UTF-8 to handle
            Unicode characters.

        .. versionadded:: 2018.3.1

    .. _`git-worktree(1)`: http://git-scm.com/docs/git-worktree

    CLI Examples:

    .. code-block:: bash

        salt myminion git.list_worktrees /path/to/repo
        salt myminion git.list_worktrees /path/to/repo all=True
        salt myminion git.list_worktrees /path/to/repo stale=True

def list_worktrees(cwd,
                   stale=False,
                   user=None,
                   password=None,
                   output_encoding=None,
                   **kwargs):
    '''
    .. versionadded:: 2015.8.0

    Returns information on worktrees

    .. versionchanged:: 2015.8.4
        Version 2.7.0 added the ``list`` subcommand to `git-worktree(1)`_ which
        provides a lot of additional information. The return data has been
        changed to include this information, even for pre-2.7.0 versions of
        git. In addition, if a worktree has a detached head, then any tags
        which point to the worktree's HEAD will be included in the return data.

    .. note::
        By default, only worktrees for which the worktree directory is still
        present are returned, but this can be changed using the ``all`` and
        ``stale`` arguments (described below).

    cwd
        The path to the git checkout

    user
        User under which to run the git command. By default, the command is run
        by the user under which the minion is running.

    password
        Windows only. Required when specifying ``user``. This parameter will be
        ignored on non-Windows platforms.

      .. versionadded:: 2016.3.4

    all : False
        If ``True``, then return all worktrees tracked under
        $GIT_DIR/worktrees, including ones for which the gitdir is no longer
        present.

    stale : False
        If ``True``, return *only* worktrees whose gitdir is no longer present.

    .. note::
        Only one of ``all`` and ``stale`` can be set to ``True``.

    output_encoding
        Use this option to specify which encoding to use to decode the output
        from any git commands which are run. This should not be needed in most
        cases.

        .. note::
            This should only be needed if the files in the repository were
            created with filenames using an encoding other than UTF-8 to handle
            Unicode characters.

        .. versionadded:: 2018.3.1

    .. _`git-worktree(1)`: http://git-scm.com/docs/git-worktree

    CLI Examples:

    .. code-block:: bash

        salt myminion git.list_worktrees /path/to/repo
        salt myminion git.list_worktrees /path/to/repo all=True
        salt myminion git.list_worktrees /path/to/repo stale=True
    '''
    if not _check_worktree_support(failhard=True):
        return {}
    cwd = _expand_path(cwd, user)
    kwargs = salt.utils.args.clean_kwargs(**kwargs)
    all_ = kwargs.pop('all', False)
    if kwargs:
        salt.utils.args.invalid_kwargs(kwargs)

    if all_ and stale:
        raise CommandExecutionError(
            '\'all\' and \'stale\' cannot both be set to True'
        )

    def _git_tag_points_at(cwd, rev, user=None, password=None,
                           output_encoding=None):
        '''
        Get any tags that point at a
        '''
        return _git_run(['git', 'tag', '--points-at', rev],
                        cwd=cwd,
                        user=user,
                        password=password,
                        output_encoding=output_encoding)['stdout'].splitlines()

    def _desired(is_stale, all_, stale):
        '''
        Common logic to determine whether or not to include the worktree info
        in the return data.
        '''
        if is_stale:
            if not all_ and not stale:
                # Stale worktrees are not desired, skip this one
                return False
        else:
            if stale:
                # Only stale worktrees are desired, skip this one
                return False
        return True

    def _duplicate_worktree_path(path):
        '''
        Log errors to the minion log notifying of duplicate worktree paths.
        These should not be there, but may show up due to a bug in git 2.7.0.
        '''
        log.error(
            'git.worktree: Duplicate worktree path %s. This may be caused by '
            'a known issue in git 2.7.0 (see '
            'http://permalink.gmane.org/gmane.comp.version-control.git/283998)',
            path
        )

    tracked_data_points = ('worktree', 'HEAD', 'branch')
    ret = {}
    git_version = _LooseVersion(version(versioninfo=False))
    has_native_list_subcommand = git_version >= _LooseVersion('2.7.0')
    if has_native_list_subcommand:
        out = _git_run(['git', 'worktree', 'list', '--porcelain'],
                       cwd=cwd,
                       user=user,
                       password=password,
                       output_encoding=output_encoding)
        if out['retcode'] != 0:
            msg = 'Failed to list worktrees'
            if out['stderr']:
                msg += ': {0}'.format(out['stderr'])
            raise CommandExecutionError(msg)

        def _untracked_item(line):
            '''
            Log a warning
            '''
            log.warning('git.worktree: Untracked line item \'%s\'', line)

        for individual_worktree in \
                salt.utils.itertools.split(out['stdout'].strip(), '\n\n'):
            # Initialize the dict where we're storing the tracked data points
            worktree_data = dict([(x, '') for x in tracked_data_points])

            for line in salt.utils.itertools.split(individual_worktree, '\n'):
                try:
                    type_, value = line.strip().split(None, 1)
                except ValueError:
                    if line == 'detached':
                        type_ = 'branch'
                        value = 'detached'
                    else:
                        _untracked_item(line)
                        continue

                if type_ not in tracked_data_points:
                    _untracked_item(line)
                    continue

                if worktree_data[type_]:
                    log.error(
                        'git.worktree: Unexpected duplicate %s entry '
                        '\'%s\', skipping', type_, line
                    )
                    continue

                worktree_data[type_] = value

            # Check for missing data points
            missing = [x for x in tracked_data_points if not worktree_data[x]]
            if missing:
                log.error(
                    'git.worktree: Incomplete worktree data, missing the '
                    'following information: %s. Full data below:\n%s',
                    ', '.join(missing), individual_worktree
                )
                continue

            worktree_is_stale = not os.path.isdir(worktree_data['worktree'])

            if not _desired(worktree_is_stale, all_, stale):
                continue

            if worktree_data['worktree'] in ret:
                _duplicate_worktree_path(worktree_data['worktree'])

            wt_ptr = ret.setdefault(worktree_data['worktree'], {})
            wt_ptr['stale'] = worktree_is_stale
            wt_ptr['HEAD'] = worktree_data['HEAD']
            wt_ptr['detached'] = worktree_data['branch'] == 'detached'
            if wt_ptr['detached']:
                wt_ptr['branch'] = None
                # Check to see if HEAD points at a tag
                tags_found = _git_tag_points_at(cwd,
                                                wt_ptr['HEAD'],
                                                user=user,
                                                password=password,
                                                output_encoding=output_encoding)
                if tags_found:
                    wt_ptr['tags'] = tags_found
            else:
                wt_ptr['branch'] = \
                    worktree_data['branch'].replace('refs/heads/', '', 1)

        return ret

    else:
        toplevel = _get_toplevel(cwd, user=user, password=password,
                                 output_encoding=output_encoding)
        try:
            worktree_root = rev_parse(cwd,
                                      opts=['--git-path', 'worktrees'],
                                      user=user,
                                      password=password,
                                      output_encoding=output_encoding)
        except CommandExecutionError as exc:
            msg = 'Failed to find worktree location for ' + cwd
            log.error(msg, exc_info_on_loglevel=logging.DEBUG)
            raise CommandExecutionError(msg)
        if worktree_root.startswith('.git'):
            worktree_root = os.path.join(cwd, worktree_root)
        if not os.path.isdir(worktree_root):
            raise CommandExecutionError(
                'Worktree admin directory {0} not present'.format(worktree_root)
            )

        def _read_file(path):
            '''
            Return contents of a single line file with EOF newline stripped
            '''
            try:
                with salt.utils.files.fopen(path, 'r') as fp_:
                    for line in fp_:
                        ret = salt.utils.stringutils.to_unicode(line).strip()
                        # Ignore other lines, if they exist (which they
                        # shouldn't)
                        break
                    return ret
            except (IOError, OSError) as exc:
                # Raise a CommandExecutionError
                salt.utils.files.process_read_exception(exc, path)

        for worktree_name in os.listdir(worktree_root):
            admin_dir = os.path.join(worktree_root, worktree_name)
            gitdir_file = os.path.join(admin_dir, 'gitdir')
            head_file = os.path.join(admin_dir, 'HEAD')

            wt_loc = _read_file(gitdir_file)
            head_ref = _read_file(head_file)

            if not os.path.isabs(wt_loc):
                log.error(
                    'Non-absolute path found in %s. If git 2.7.0 was '
                    'installed and then downgraded, this was likely caused '
                    'by a known issue in git 2.7.0. See '
                    'http://permalink.gmane.org/gmane.comp.version-control'
                    '.git/283998 for more information.', gitdir_file
                )
                # Emulate what 'git worktree list' does under-the-hood, and
                # that is using the toplevel directory. It will still give
                # inaccurate results, but will avoid a traceback.
                wt_loc = toplevel

            if wt_loc.endswith('/.git'):
                wt_loc = wt_loc[:-5]

            worktree_is_stale = not os.path.isdir(wt_loc)

            if not _desired(worktree_is_stale, all_, stale):
                continue

            if wt_loc in ret:
                _duplicate_worktree_path(wt_loc)

            if head_ref.startswith('ref: '):
                head_ref = head_ref.split(None, 1)[-1]
                wt_branch = head_ref.replace('refs/heads/', '', 1)
                wt_head = rev_parse(cwd,
                                    rev=head_ref,
                                    user=user,
                                    password=password,
                                    output_encoding=output_encoding)
                wt_detached = False
            else:
                wt_branch = None
                wt_head = head_ref
                wt_detached = True

            wt_ptr = ret.setdefault(wt_loc, {})
            wt_ptr['stale'] = worktree_is_stale
            wt_ptr['branch'] = wt_branch
            wt_ptr['HEAD'] = wt_head
            wt_ptr['detached'] = wt_detached

            # Check to see if HEAD points at a tag
            if wt_detached:
                tags_found = _git_tag_points_at(cwd,
                                                wt_head,
                                                user=user,
                                                password=password,
                                                output_encoding=output_encoding)
                if tags_found:
                    wt_ptr['tags'] = tags_found

    return ret