def latest(name,
           rev='HEAD',
           target=None,
           branch=None,
           user=None,
           update_head=True,
           force_checkout=False,
           force_clone=False,
           force_fetch=False,
           force_reset=False,
           submodules=False,
           bare=False,
           mirror=False,
           remote='origin',
           fetch_tags=True,
           depth=None,
           identity=None,
           https_user=None,
           https_pass=None,
           onlyif=False,
           unless=False,
           **kwargs):
    '''
    Make sure the repository is cloned to the given directory and is
    up-to-date.

    name
        Address of the remote repository as passed to "git clone"

    rev : HEAD
        The remote branch, tag, or revision ID to checkout after clone / before
        update. If specified, then Salt will also ensure that the tracking
        branch is set to ``<remote>/<rev>``, unless ``rev`` refers to a tag or
        SHA1, in which case Salt will ensure that the tracking branch is unset.

        If ``rev`` is not specified, it will be assumed to be ``HEAD``, and
        Salt will not manage the tracking branch at all.

        .. versionchanged:: 2015.8.0
            If not specified, ``rev`` now defaults to the remote repository's
            HEAD.

    target
        Name of the target directory where repository is about to be cloned

    branch
        Name of the branch into which to checkout the specified rev. If not
        specified, then Salt will not care what branch is being used locally
        and will just use whatever branch is currently there.

        .. note::
            If not specified, this means that the local branch name will not be
            changed if the repository is reset to another branch/tag/SHA1.

        .. versionadded:: 2015.8.0

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

        .. versionadded:: 0.17.0

    update_head : True
        If set to ``False``, then the remote repository will be fetched (if
        necessary) to ensure that the commit to which ``rev`` points exists in
        the local checkout, but no changes will be made to the local HEAD.

        .. versionadded:: 2015.8.3

    force : False
        .. deprecated:: 2015.8.0
            Use ``force_clone`` instead. For earlier Salt versions, ``force``
            must be used.

    force_checkout : False
        When checking out the local branch, the state will fail if there are
        unwritten changes. Set this argument to ``True`` to discard unwritten
        changes when checking out.

    force_clone : False
        If the ``target`` directory exists and is not a git repository, then
        this state will fail. Set this argument to ``True`` to remove the
        contents of the target directory and clone the repo into it.

    force_fetch : False
        If a fetch needs to be performed, non-fast-forward fetches will cause
        this state to fail. Set this argument to ``True`` to force the fetch
        even if it is a non-fast-forward update.

        .. versionadded:: 2015.8.0

    force_reset : False
        If the update is not a fast-forward, this state will fail. Set this
        argument to ``True`` to force a hard-reset to the remote revision in
        these cases.

    submodules : False
        Update submodules on clone or branch change

    bare : False
        Set to ``True`` if the repository is to be a bare clone of the remote
        repository.

        .. note:

            Setting this option to ``True`` is incompatible with the ``rev``
            argument.

    mirror
        Set to ``True`` if the repository is to be a mirror of the remote
        repository. This implies that ``bare`` set to ``True``, and thus is
        incompatible with ``rev``.

    remote : origin
        Git remote to use. If this state needs to clone the repo, it will clone
        it using this value as the initial remote name. If the repository
        already exists, and a remote by this name is not present, one will be
        added.

    remote_name
        .. deprecated:: 2015.8.0
            Use ``remote`` instead. For earlier Salt versions, ``remote_name``
            must be used.

    fetch_tags : True
        If ``True``, then when a fetch is performed all tags will be fetched,
        even those which are not reachable by any branch on the remote.

    depth
        Defines depth in history when git a clone is needed in order to ensure
        latest. E.g. ``depth: 1`` is useful when deploying from a repository
        with a long history. Use rev to specify branch. This is not compatible
        with tags or revision IDs.

    identity
        Path to a private key to use for ssh URLs. This can be either a single
        string, or a list of strings. For example:

        .. code-block:: yaml

            # Single key
            git@github.com:user/repo.git:
              git.latest:
                - user: deployer
                - identity: /home/deployer/.ssh/id_rsa

            # Two keys
            git@github.com:user/repo.git:
              git.latest:
                - user: deployer
                - identity:
                  - /home/deployer/.ssh/id_rsa
                  - /home/deployer/.ssh/id_rsa_alternate

        If multiple keys are specified, they will be tried one-by-one in order
        for each git command which needs to authenticate.

        .. warning::

            Unless Salt is invoked from the minion using ``salt-call``, the
            key(s) must be passphraseless. For greater security with
            passphraseless private keys, see the `sshd(8)`_ manpage for
            information on securing the keypair from the remote side in the
            ``authorized_keys`` file.

            .. _`sshd(8)`: http://www.man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE%20FORMAT

        .. versionchanged:: 2015.8.7
            Salt will no longer attempt to use passphrase-protected keys unless
            invoked from the minion using ``salt-call``, to prevent blocking
            waiting for user input.

        Key can be specified as a SaltStack file server URL, eg. salt://location/identity_file

        .. versionadded:: 2016.3.0

    https_user
        HTTP Basic Auth username for HTTPS (only) clones

        .. versionadded:: 2015.5.0

    https_pass
        HTTP Basic Auth password for HTTPS (only) clones

        .. versionadded:: 2015.5.0

    onlyif
        A command to run as a check, run the named command only if the command
        passed to the ``onlyif`` option returns true

    unless
        A command to run as a check, only run the named command if the command
        passed to the ``unless`` option returns false

    .. note::
        Clashing ID declarations can be avoided when including different
        branches from the same git repository in the same sls file by using the
        ``name`` declaration.  The example below checks out the ``gh-pages``
        and ``gh-pages-prod`` branches from the same repository into separate
        directories.  The example also sets up the ``ssh_known_hosts`` ssh key
        required to perform the git checkout.

    .. code-block:: yaml

        gitlab.example.com:
          ssh_known_hosts:
            - present
            - user: root
            - enc: ecdsa
            - fingerprint: 4e:94:b0:54:c1:5b:29:a2:70:0e:e1:a3:51:ee:ee:e3

        git-website-staging:
          git.latest:
            - name: ssh://git@gitlab.example.com:user/website.git
            - rev: gh-pages
            - target: /usr/share/nginx/staging
            - identity: /root/.ssh/website_id_rsa
            - require:
              - pkg: git
              - ssh_known_hosts: gitlab.example.com

        git-website-staging:
          git.latest:
            - name: ssh://git@gitlab.example.com:user/website.git
            - rev: gh-pages
            - target: /usr/share/nginx/staging
            - identity: salt://website/id_rsa
            - require:
              - pkg: git
              - ssh_known_hosts: gitlab.example.com

            .. versionadded:: 2016.3.0

        git-website-prod:
          git.latest:
            - name: ssh://git@gitlab.example.com:user/website.git
            - rev: gh-pages-prod
            - target: /usr/share/nginx/prod
            - identity: /root/.ssh/website_id_rsa
            - require:
              - pkg: git
              - ssh_known_hosts: gitlab.example.com
    '''
    ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}

    kwargs = salt.utils.clean_kwargs(**kwargs)
    always_fetch = kwargs.pop('always_fetch', False)
    force = kwargs.pop('force', False)
    remote_name = kwargs.pop('remote_name', False)
    if kwargs:
        return _fail(
            ret,
            salt.utils.invalid_kwargs(kwargs, raise_exc=False)
        )

    if always_fetch:
        salt.utils.warn_until(
            'Nitrogen',
            'The \'always_fetch\' argument to the git.latest state no longer '
            'has any effect, see the 2015.8.0 release notes for details.'
        )
    if force:
        salt.utils.warn_until(
            'Nitrogen',
            'The \'force\' argument to the git.latest state has been '
            'deprecated, please use \'force_clone\' instead.'
        )
        force_clone = force
    if remote_name:
        salt.utils.warn_until(
            'Nitrogen',
            'The \'remote_name\' argument to the git.latest state has been '
            'deprecated, please use \'remote\' instead.'
        )
        remote = remote_name

    if not remote:
        return _fail(ret, '\'remote\' argument is required')

    if not target:
        return _fail(ret, '\'target\' argument is required')

    if not rev:
        return _fail(
            ret,
            '\'{0}\' is not a valid value for the \'rev\' argument'.format(rev)
        )

    # Ensure that certain arguments are strings to ensure that comparisons work
    if not isinstance(rev, six.string_types):
        rev = str(rev)
    if target is not None:
        if not isinstance(target, six.string_types):
            target = str(target)
        if not os.path.isabs(target):
            return _fail(
                ret,
                'target \'{0}\' is not an absolute path'.format(target)
            )
    if branch is not None and not isinstance(branch, six.string_types):
        branch = str(branch)
    if user is not None and not isinstance(user, six.string_types):
        user = str(user)
    if remote is not None and not isinstance(remote, six.string_types):
        remote = str(remote)
    if identity is not None:
        if isinstance(identity, six.string_types):
            identity = [identity]
        elif not isinstance(identity, list):
            return _fail(ret, 'identity must be either a list or a string')
        for ident_path in identity:
            if 'salt://' in ident_path:
                try:
                    ident_path = __salt__['cp.cache_file'](ident_path)
                except IOError as exc:
                    log.error(
                        'Failed to cache {0}: {1}'.format(ident_path, exc)
                    )
                    return _fail(
                        ret,
                        'identity \'{0}\' does not exist.'.format(
                            ident_path
                        )
                    )
            if not os.path.isabs(ident_path):
                return _fail(
                    ret,
                    'identity \'{0}\' is not an absolute path'.format(
                        ident_path
                    )
                )
    if https_user is not None and not isinstance(https_user, six.string_types):
        https_user = str(https_user)
    if https_pass is not None and not isinstance(https_pass, six.string_types):
        https_pass = str(https_pass)

    if os.path.isfile(target):
        return _fail(
            ret,
            'Target \'{0}\' exists and is a regular file, cannot proceed'
            .format(target)
        )

    try:
        desired_fetch_url = salt.utils.url.add_http_basic_auth(
            name,
            https_user,
            https_pass,
            https_only=True
        )
    except ValueError as exc:
        return _fail(ret, exc.__str__())

    redacted_fetch_url = \
        salt.utils.url.redact_http_basic_auth(desired_fetch_url)

    if mirror:
        bare = True

    # Check to make sure rev and mirror/bare are not both in use
    if rev != 'HEAD' and bare:
        return _fail(ret, ('\'rev\' is not compatible with the \'mirror\' and '
                           '\'bare\' arguments'))

    run_check_cmd_kwargs = {'runas': user}
    if 'shell' in __grains__:
        run_check_cmd_kwargs['shell'] = __grains__['shell']

    # check if git.latest should be applied
    cret = mod_run_check(
        run_check_cmd_kwargs, onlyif, unless
    )
    if isinstance(cret, dict):
        ret.update(cret)
        return ret

    refspecs = [
        'refs/heads/*:refs/remotes/{0}/*'.format(remote),
        '+refs/tags/*:refs/tags/*'
    ] if fetch_tags else []

    log.info('Checking remote revision for {0}'.format(name))
    try:
        all_remote_refs = __salt__['git.remote_refs'](
            name,
            heads=False,
            tags=False,
            user=user,
            identity=identity,
            https_user=https_user,
            https_pass=https_pass,
            ignore_retcode=False)
    except CommandExecutionError as exc:
        return _fail(
            ret,
            'Failed to check remote refs: {0}'.format(_strip_exc(exc))
        )

    if bare:
        remote_rev = None
        remote_rev_type = None
    else:
        if rev == 'HEAD':
            if 'HEAD' in all_remote_refs:
                remote_rev = all_remote_refs['HEAD']
                # Just go with whatever the upstream currently is
                desired_upstream = None
                remote_rev_type = 'sha1'
            else:
                # Empty remote repo
                remote_rev = None
                remote_rev_type = None
        elif 'refs/heads/' + rev in all_remote_refs:
            remote_rev = all_remote_refs['refs/heads/' + rev]
            desired_upstream = '/'.join((remote, rev))
            remote_rev_type = 'branch'
        elif 'refs/tags/' + rev + '^{}' in all_remote_refs:
            # Annotated tag
            remote_rev = all_remote_refs['refs/tags/' + rev + '^{}']
            desired_upstream = False
            remote_rev_type = 'tag'
        elif 'refs/tags/' + rev in all_remote_refs:
            # Non-annotated tag
            remote_rev = all_remote_refs['refs/tags/' + rev]
            desired_upstream = False
            remote_rev_type = 'tag'
        else:
            if len(rev) <= 40 \
                    and all(x in string.hexdigits for x in rev):
                # git ls-remote did not find the rev, and because it's a
                # hex string <= 40 chars we're going to assume that the
                # desired rev is a SHA1
                rev = rev.lower()
                remote_rev = rev
                desired_upstream = False
                remote_rev_type = 'sha1'
            else:
                remote_rev = None
                remote_rev_type = None

        # For the comment field of the state return dict, the remote location
        # (and short-sha1, if rev is not a sha1) is referenced several times,
        # determine it once here and reuse the value below.
        if remote_rev_type == 'sha1':
            if rev == 'HEAD':
                remote_loc = 'remote HEAD (' + remote_rev[:7] + ')'
            else:
                remote_loc = remote_rev[:7]
        elif remote_rev is not None:
            remote_loc = '{0} ({1})'.format(
                desired_upstream if remote_rev_type == 'branch' else rev,
                remote_rev[:7]
            )
        else:
            # Shouldn't happen but log a warning here for future
            # troubleshooting purposes in the event we find a corner case.
            log.warning(
                'Unable to determine remote_loc. rev is %s, remote_rev is '
                '%s, remove_rev_type is %s, desired_upstream is %s, and bare '
                'is%s set',
                rev,
                remote_rev,
                remote_rev_type,
                desired_upstream,
                ' not' if not bare else ''
            )
            remote_loc = None

    if remote_rev is None and not bare:
        if rev != 'HEAD':
            # A specific rev is desired, but that rev doesn't exist on the
            # remote repo.
            return _fail(
                ret,
                'No revision matching \'{0}\' exists in the remote '
                'repository'.format(rev)
            )

    git_ver = _LooseVersion(__salt__['git.version'](versioninfo=False))

    check = 'refs' if bare else '.git'
    gitdir = os.path.join(target, check)
    comments = []
    if os.path.isdir(gitdir) or __salt__['git.is_worktree'](target):
        # Target directory is a git repository or git worktree
        try:
            all_local_branches = __salt__['git.list_branches'](
                target, user=user)
            all_local_tags = __salt__['git.list_tags'](target, user=user)
            local_rev, local_branch = _get_local_rev_and_branch(target, user)

            if not bare and remote_rev is None and local_rev is not None:
                return _fail(
                    ret,
                    'Remote repository is empty, cannot update from a '
                    'non-empty to an empty repository'
                )

            # Base rev and branch are the ones from which any reset or merge
            # will take place. If the branch is not being specified, the base
            # will be the "local" rev and branch, i.e. those we began with
            # before this state was run. If a branch is being specified and it
            # both exists and is not the one with which we started, then we'll
            # be checking that branch out first, and it instead becomes our
            # base. The base branch and rev will be used below in comparisons
            # to determine what changes to make.
            base_rev = local_rev
            base_branch = local_branch
            if _need_branch_change(branch, local_branch):
                if branch not in all_local_branches:
                    # We're checking out a new branch, so the base_rev and
                    # remote_rev will be identical.
                    base_rev = remote_rev
                else:
                    base_branch = branch
                    # Desired branch exists locally and is not the current
                    # branch. We'll be performing a checkout to that branch
                    # eventually, but before we do that we need to find the
                    # current SHA1.
                    try:
                        base_rev = __salt__['git.rev_parse'](
                            target,
                            branch + '^{commit}',
                            user=user,
                            ignore_retcode=True)
                    except CommandExecutionError as exc:
                        return _fail(
                            ret,
                            'Unable to get position of local branch \'{0}\': '
                            '{1}'.format(branch, _strip_exc(exc)),
                            comments
                        )

            remotes = __salt__['git.remotes'](target,
                                              user=user,
                                              redact_auth=False)

            revs_match = _revs_equal(local_rev, remote_rev, remote_rev_type)

            if remote_rev_type == 'sha1' \
                    and base_rev is not None \
                    and base_rev.startswith(remote_rev):
                # Either we're already checked out to the branch we need and it
                # is up-to-date, or the branch to which we need to switch is
                # on the same SHA1 as the desired remote revision. Either way,
                # we know we have the remote rev present already and no fetch
                # will be needed.
                has_remote_rev = True
            else:
                has_remote_rev = False
                if remote_rev is not None:
                    try:
                        __salt__['git.rev_parse'](
                            target,
                            remote_rev + '^{commit}',
                            ignore_retcode=True)
                    except CommandExecutionError:
                        # Local checkout doesn't have the remote_rev
                        pass
                    else:
                        # The object might exist enough to get a rev-parse to
                        # work, while the local ref could have been
                        # deleted/changed/force updated. Do some further sanity
                        # checks to determine if we really do have the
                        # remote_rev.
                        if remote_rev_type == 'branch':
                            if remote in remotes:
                                try:
                                    # Do a rev-parse on <remote>/<rev> to get
                                    # the local SHA1 for it, so we can compare
                                    # it to the remote_rev SHA1.
                                    local_copy = __salt__['git.rev_parse'](
                                        target,
                                        desired_upstream,
                                        user=user,
                                        ignore_retcode=True)
                                except CommandExecutionError:
                                    pass
                                else:
                                    # If the SHA1s don't match, then the remote
                                    # branch was force-updated, and we need to
                                    # fetch to update our local copy the ref
                                    # for the remote branch. If they do match,
                                    # then we have the remote_rev and don't
                                    # need to fetch.
                                    if local_copy == remote_rev:
                                        has_remote_rev = True
                        elif remote_rev_type == 'tag':
                            if rev in all_local_tags:
                                try:
                                    local_tag_sha1 = __salt__['git.rev_parse'](
                                        target,
                                        rev + '^{commit}',
                                        user=user,
                                        ignore_retcode=True)
                                except CommandExecutionError:
                                    # Shouldn't happen if the tag exists
                                    # locally but account for this just in
                                    # case.
                                    local_tag_sha1 = None
                                if local_tag_sha1 == remote_rev:
                                    has_remote_rev = True
                                else:
                                    if not force_reset:
                                        # SHA1 of tag on remote repo is
                                        # different than local tag. Unless
                                        # we're doing a hard reset then we
                                        # don't need to proceed as we know that
                                        # the fetch will update the tag and the
                                        # only way to make the state succeed is
                                        # to reset the branch to point at the
                                        # tag's new location.
                                        return _fail(
                                            ret,
                                            '\'{0}\' is a tag, but the remote '
                                            'SHA1 for this tag ({1}) doesn\'t '
                                            'match the local SHA1 ({2}). Set '
                                            '\'force_reset\' to True to force '
                                            'this update.'.format(
                                                rev,
                                                _short_sha(remote_rev),
                                                _short_sha(local_tag_sha1)
                                            )
                                        )
                        elif remote_rev_type == 'sha1':
                            has_remote_rev = True

            if not has_remote_rev:
                # Either the remote rev could not be found with git
                # ls-remote (in which case we won't know more until
                # fetching) or we're going to be checking out a new branch
                # and don't have to worry about fast-forwarding.
                fast_forward = None
            else:
                # Remote rev already present
                if (not revs_match and not update_head) \
                        and (branch is None or branch == local_branch):
                    ret['comment'] = remote_loc.capitalize() \
                        if rev == 'HEAD' \
                        else remote_loc
                    ret['comment'] += (
                        ' is already present and local HEAD ({0}) does not '
                        'match, but update_head=False. HEAD has not been '
                        'updated locally.'.format(local_rev[:7])
                    )
                    return ret

                if base_rev is None:
                    # If we're here, the remote_rev exists in the local
                    # checkout but there is still no HEAD locally. A possible
                    # reason for this is that an empty repository existed there
                    # and a remote was added and fetched, but the repository
                    # was not fast-forwarded. Regardless, going from no HEAD to
                    # a locally-present rev is considered a fast-forward update.
                    fast_forward = True
                else:
                    fast_forward = __salt__['git.merge_base'](
                        target,
                        refs=[base_rev, remote_rev],
                        is_ancestor=True,
                        user=user,
                        ignore_retcode=True)

            if fast_forward is False:
                if not force_reset:
                    return _not_fast_forward(
                        ret,
                        base_rev,
                        remote_rev,
                        branch,
                        local_branch,
                        comments)
                merge_action = 'hard-reset'
            elif fast_forward is True:
                merge_action = 'fast-forwarded'
            else:
                merge_action = 'updated'

            if base_branch is None:
                # No local branch, no upstream tracking branch
                upstream = None
            else:
                try:
                    upstream = __salt__['git.rev_parse'](
                        target,
                        base_branch + '@{upstream}',
                        opts=['--abbrev-ref'],
                        user=user,
                        ignore_retcode=True)
                except CommandExecutionError:
                    # There is a local branch but the rev-parse command
                    # failed, so that means there is no upstream tracking
                    # branch. This could be because it is just not set, or
                    # because the branch was checked out to a SHA1 or tag
                    # instead of a branch. Set upstream to False to make a
                    # distinction between the case above where there is no
                    # local_branch (when the local checkout is an empty
                    # repository).
                    upstream = False

            if remote in remotes:
                fetch_url = remotes[remote]['fetch']
            else:
                log.debug(
                    'Remote \'{0}\' not found in git checkout at {1}'
                    .format(remote, target)
                )
                fetch_url = None

            if remote_rev is not None and desired_fetch_url != fetch_url:
                if __opts__['test']:
                    actions = [
                        'Remote \'{0}\' would be changed from {1} to {2}'
                        .format(
                            remote,
                            salt.utils.url.redact_http_basic_auth(fetch_url),
                            redacted_fetch_url
                        )
                    ]
                    if not has_remote_rev:
                        actions.append('Remote would be fetched')
                    if not revs_match:
                        if update_head:
                            ret['changes']['revision'] = {
                                'old': local_rev, 'new': remote_rev
                            }
                            if fast_forward is False:
                                ret['changes']['forced update'] = True
                            actions.append(
                                'Repository would be {0} to {1}'.format(
                                    merge_action,
                                    _short_sha(remote_rev)
                                )
                            )
                    if ret['changes']:
                        return _neutral_test(ret, _format_comments(actions))
                    else:
                        if not revs_match and not update_head:
                            # Repo content would not be modified but the remote
                            # URL would be modified, so we can't just say that
                            # the repo is up-to-date, we need to inform the
                            # user of the actions taken.
                            ret['comment'] = _format_comments(actions)
                            return ret
                        return _uptodate(ret,
                                         target,
                                         _format_comments(actions))

                # The fetch_url for the desired remote does not match the
                # specified URL (or the remote does not exist), so set the
                # remote URL.
                __salt__['git.remote_set'](target,
                                           url=name,
                                           remote=remote,
                                           user=user,
                                           https_user=https_user,
                                           https_pass=https_pass)
                comments.append(
                    'Remote \'{0}\' changed from {1} to {2}'.format(
                        remote,
                        salt.utils.url.redact_http_basic_auth(fetch_url),
                        redacted_fetch_url
                    )
                )

            if remote_rev is not None:
                if __opts__['test']:
                    actions = []
                    if not has_remote_rev:
                        actions.append(
                            'Remote \'{0}\' would be fetched'.format(remote)
                        )
                    if (not revs_match) \
                            and (update_head or (branch is not None
                                                 and branch != local_branch)):
                        ret['changes']['revision'] = {
                            'old': local_rev, 'new': remote_rev
                        }
                    if _need_branch_change(branch, local_branch):
                        if branch not in all_local_branches:
                            actions.append(
                                'New branch \'{0}\' would be checked '
                                'out, with {1} as a starting '
                                'point'.format(branch, remote_loc)
                            )
                            if desired_upstream:
                                actions.append(
                                    'Tracking branch would be set to {0}'
                                    .format(desired_upstream)
                                )
                        else:
                            actions.append(
                                'Branch \'{0}\' would be checked out '
                                'and {1} to {2}'.format(
                                    branch,
                                    merge_action,
                                    _short_sha(remote_rev)
                                )
                            )
                    else:
                        if not revs_match:
                            if update_head:
                                if fast_forward is True:
                                    actions.append(
                                        'Repository would be fast-forwarded from '
                                        '{0} to {1}'.format(
                                            _short_sha(local_rev),
                                            _short_sha(remote_rev)
                                        )
                                    )
                                else:
                                    actions.append(
                                        'Repository would be {0} from {1} to {2}'
                                        .format(
                                            'hard-reset'
                                                if force_reset and has_remote_rev
                                                else 'updated',
                                            _short_sha(local_rev),
                                            _short_sha(remote_rev)
                                        )
                                    )
                            else:
                                actions.append(
                                    'Local HEAD ({0}) does not match {1} but '
                                    'update_head=False, HEAD would not be '
                                    'updated locally'.format(
                                        local_rev[:7],
                                        remote_loc
                                    )
                                )

                    # Check if upstream needs changing
                    if not upstream and desired_upstream:
                        actions.append(
                            'Tracking branch would be set to {0}'.format(
                                desired_upstream
                            )
                        )
                    elif upstream and desired_upstream is False:
                        actions.append(
                            'Tracking branch would be unset'
                        )
                    elif desired_upstream and upstream != desired_upstream:
                        actions.append(
                            'Tracking branch would be '
                            'updated to {0}'.format(desired_upstream)
                        )
                    if ret['changes']:
                        return _neutral_test(ret, _format_comments(actions))
                    else:
                        formatted_actions = _format_comments(actions)
                        if not revs_match \
                                and not update_head \
                                and formatted_actions:
                            ret['comment'] = formatted_actions
                            return ret
                        return _uptodate(ret,
                                         target,
                                         _format_comments(actions))

                if not upstream and desired_upstream:
                    upstream_action = (
                        'Tracking branch was set to {0}'.format(
                            desired_upstream
                        )
                    )
                    branch_opts = _get_branch_opts(
                        branch,
                        local_branch,
                        all_local_branches,
                        desired_upstream,
                        git_ver)
                elif upstream and desired_upstream is False:
                    # If the remote_rev is a tag or SHA1, and there is an
                    # upstream tracking branch, we will unset it. However, we
                    # can only do this if the git version is 1.8.0 or newer, as
                    # the --unset-upstream option was not added until that
                    # version.
                    if git_ver >= _LooseVersion('1.8.0'):
                        upstream_action = 'Tracking branch was unset'
                        branch_opts = ['--unset-upstream']
                    else:
                        branch_opts = None
                elif desired_upstream and upstream != desired_upstream:
                    upstream_action = (
                        'Tracking branch was updated to {0}'.format(
                            desired_upstream
                        )
                    )
                    branch_opts = _get_branch_opts(
                        branch,
                        local_branch,
                        all_local_branches,
                        desired_upstream,
                        git_ver)
                else:
                    branch_opts = None

                if not has_remote_rev:
                    try:
                        fetch_changes = __salt__['git.fetch'](
                            target,
                            remote=remote,
                            force=force_fetch,
                            refspecs=refspecs,
                            user=user,
                            identity=identity)
                    except CommandExecutionError as exc:
                        return _failed_fetch(ret, exc, comments)
                    else:
                        if fetch_changes:
                            comments.append(
                                '{0} was fetched, resulting in updated '
                                'refs'.format(name)
                            )

                    try:
                        __salt__['git.rev_parse'](
                            target,
                            remote_rev + '^{commit}',
                            ignore_retcode=True)
                    except CommandExecutionError as exc:
                        return _fail(
                            ret,
                            'Fetch did not successfully retrieve rev \'{0}\' '
                            'from {1}: {2}'.format(rev, name, exc)
                        )

                    if (not revs_match and not update_head) \
                            and (branch is None or branch == local_branch):
                        # Rev now exists locally (was fetched), and since we're
                        # not updating HEAD we'll just exit here.
                        ret['comment'] = remote_loc.capitalize() \
                            if rev == 'HEAD' \
                            else remote_loc
                        ret['comment'] += (
                            ' is already present and local HEAD ({0}) does not '
                            'match, but update_head=False. HEAD has not been '
                            'updated locally.'.format(local_rev[:7])
                        )
                        return ret

                    # Now that we've fetched, check again whether or not
                    # the update is a fast-forward.
                    if base_rev is None:
                        fast_forward = True
                    else:
                        fast_forward = __salt__['git.merge_base'](
                            target,
                            refs=[base_rev, remote_rev],
                            is_ancestor=True,
                            user=user)

                    if fast_forward is False and not force_reset:
                        return _not_fast_forward(
                            ret,
                            base_rev,
                            remote_rev,
                            branch,
                            local_branch,
                            comments)

                if _need_branch_change(branch, local_branch):
                    local_changes = __salt__['git.status'](target,
                                                           user=user)
                    if local_changes and not force_checkout:
                        return _fail(
                            ret,
                            'Local branch \'{0}\' has uncommitted '
                            'changes. Set \'force_checkout\' to True to '
                            'discard them and proceed.'.format(local_branch)
                        )

                    # TODO: Maybe re-retrieve all_local_branches to handle
                    # the corner case where the destination branch was
                    # added to the local checkout during a fetch that takes
                    # a long time to complete.
                    if branch not in all_local_branches:
                        if rev == 'HEAD':
                            checkout_rev = remote_rev
                        else:
                            checkout_rev = desired_upstream \
                                if desired_upstream \
                                else rev
                        checkout_opts = ['-b', branch]
                    else:
                        checkout_rev = branch
                        checkout_opts = []
                    __salt__['git.checkout'](target,
                                             checkout_rev,
                                             force=force_checkout,
                                             opts=checkout_opts,
                                             user=user)
                    if '-b' in checkout_opts:
                        comments.append(
                            'New branch \'{0}\' was checked out, with {1} '
                            'as a starting point'.format(
                                branch,
                                remote_loc
                            )
                        )
                    else:
                        comments.append(
                            '\'{0}\' was checked out'.format(checkout_rev)
                        )

                if fast_forward is False:
                    __salt__['git.reset'](
                        target,
                        opts=['--hard', remote_rev],
                        user=user
                    )
                    ret['changes']['forced update'] = True
                    comments.append(
                        'Repository was hard-reset to {0}'.format(remote_loc)
                    )

                if branch_opts is not None:
                    __salt__['git.branch'](
                        target,
                        opts=branch_opts,
                        user=user)
                    comments.append(upstream_action)

                # Fast-forward to the desired revision
                if fast_forward is True \
                        and not _revs_equal(base_rev,
                                            remote_rev,
                                            remote_rev_type):
                    if desired_upstream or rev == 'HEAD':
                        # Check first to see if we are on a branch before
                        # trying to merge changes. (The call to
                        # git.symbolic_ref will only return output if HEAD
                        # points to a branch.)
                        if __salt__['git.symbolic_ref'](target,
                                                        'HEAD',
                                                        opts=['--quiet'],
                                                        ignore_retcode=True):
                            merge_rev = remote_rev if rev == 'HEAD' \
                                else desired_upstream

                            if git_ver >= _LooseVersion('1.8.1.6'):
                                # --ff-only added in version 1.8.1.6. It's not
                                # 100% necessary, but if we can use it, we'll
                                # ensure that the merge doesn't go through if
                                # not a fast-forward. Granted, the logic that
                                # gets us to this point shouldn't allow us to
                                # attempt this merge if it's not a
                                # fast-forward, but it's an extra layer of
                                # protection.
                                merge_opts = ['--ff-only']
                            else:
                                merge_opts = []

                            __salt__['git.merge'](
                                target,
                                rev=merge_rev,
                                opts=merge_opts,
                                user=user
                            )
                            comments.append(
                                'Repository was fast-forwarded to {0}'
                                .format(remote_loc)
                            )
                        else:
                            return _fail(
                                ret,
                                'Unable to fast-forward, HEAD is detached',
                                comments
                            )
                    else:
                        # Update is a fast forward, but we cannot merge to that
                        # commit so we'll reset to it.
                        __salt__['git.reset'](
                            target,
                            opts=['--hard',
                                  remote_rev if rev == 'HEAD' else rev],
                            user=user
                        )
                        comments.append(
                            'Repository was reset to {0} (fast-forward)'
                            .format(rev)
                        )

                # TODO: Figure out how to add submodule update info to
                # test=True return data, and changes dict.
                if submodules:
                    try:
                        __salt__['git.submodule'](
                            target,
                            'update',
                            opts=['--init', '--recursive'],
                            user=user,
                            identity=identity)
                    except CommandExecutionError as exc:
                        return _failed_submodule_update(ret, exc, comments)
            elif bare:
                if __opts__['test']:
                    msg = (
                        'Bare repository at {0} would be fetched'
                        .format(target)
                    )
                    if ret['changes']:
                        return _neutral_test(ret, msg)
                    else:
                        return _uptodate(ret, target, msg)
                try:
                    fetch_changes = __salt__['git.fetch'](
                        target,
                        remote=remote,
                        force=force_fetch,
                        refspecs=refspecs,
                        user=user,
                        identity=identity)
                except CommandExecutionError as exc:
                    return _failed_fetch(ret, exc, comments)
                else:
                    comments.append(
                        'Bare repository at {0} was fetched{1}'.format(
                            target,
                            ', resulting in updated refs'
                                if fetch_changes
                                else ''
                        )
                    )
            try:
                new_rev = __salt__['git.revision'](
                    cwd=target,
                    user=user,
                    ignore_retcode=True)
            except CommandExecutionError:
                new_rev = None

        except Exception as exc:
            log.error(
                'Unexpected exception in git.latest state',
                exc_info=True
            )
            if isinstance(exc, CommandExecutionError):
                msg = _strip_exc(exc)
            else:
                msg = str(exc)
            return _fail(ret, msg, comments)

        if not bare and not _revs_equal(new_rev,
                                        remote_rev,
                                        remote_rev_type):
            return _fail(ret, 'Failed to update repository', comments)

        if local_rev != new_rev:
            log.info(
                'Repository {0} updated: {1} => {2}'.format(
                    target, local_rev, new_rev)
            )
            ret['comment'] = _format_comments(comments)
            ret['changes']['revision'] = {'old': local_rev, 'new': new_rev}
        else:
            return _uptodate(ret, target, _format_comments(comments))
    else:
        if os.path.isdir(target):
            if force_clone:
                # Clone is required, and target directory exists, but the
                # ``force`` option is enabled, so we need to clear out its
                # contents to proceed.
                if __opts__['test']:
                    ret['changes']['forced clone'] = True
                    ret['changes']['new'] = name + ' => ' + target
                    return _neutral_test(
                        ret,
                        'Target directory {0} exists. Since force_clone=True, '
                        'the contents of {0} would be deleted, and {1} would '
                        'be cloned into this directory.'.format(target, name)
                    )
                log.debug(
                    'Removing contents of {0} to clone repository {1} in its '
                    'place (force_clone=True set in git.latest state)'
                    .format(target, name)
                )
                try:
                    if os.path.islink(target):
                        os.unlink(target)
                    else:
                        salt.utils.rm_rf(target)
                except __HOLE__ as exc:
                    return _fail(
                        ret,
                        'Unable to remove {0}: {1}'.format(target, exc),
                        comments
                    )
                else:
                    ret['changes']['forced clone'] = True
            # Clone is required, but target dir exists and is non-empty. We
            # can't proceed.
            elif os.listdir(target):
                return _fail(
                    ret,
                    'Target \'{0}\' exists, is non-empty and is not a git '
                    'repository. Set the \'force_clone\' option to True to '
                    'remove this directory\'s contents and proceed with '
                    'cloning the remote repository'.format(target)
                )

        log.debug(
            'Target {0} is not found, \'git clone\' is required'.format(target)
        )
        if __opts__['test']:
            ret['changes']['new'] = name + ' => ' + target
            return _neutral_test(
                ret,
                'Repository {0} would be cloned to {1}'.format(
                    name, target
                )
            )
        try:
            clone_opts = ['--mirror'] if mirror else ['--bare'] if bare else []
            if remote != 'origin':
                clone_opts.extend(['--origin', remote])
            if depth is not None:
                clone_opts.extend(['--depth', str(depth)])

            # We're cloning a fresh repo, there is no local branch or revision
            local_branch = local_rev = None

            try:
                __salt__['git.clone'](target,
                                      name,
                                      user=user,
                                      opts=clone_opts,
                                      identity=identity,
                                      https_user=https_user,
                                      https_pass=https_pass)
            except CommandExecutionError as exc:
                msg = 'Clone failed: {0}'.format(_strip_exc(exc))
                return _fail(ret, msg, comments)

            ret['changes']['new'] = name + ' => ' + target
            comments.append(
                '{0} cloned to {1}{2}'.format(
                    name,
                    target,
                    ' as mirror' if mirror
                        else ' as bare repository' if bare
                        else ''
                )
            )

            if not bare:
                if not remote_rev:
                    if rev != 'HEAD':
                        # No HEAD means the remote repo is empty, which means
                        # our new clone will also be empty. This state has
                        # failed, since a rev was specified but no matching rev
                        # exists on the remote host.
                        msg = (
                            '{{0}} was cloned but is empty, so {0}/{1} '
                            'cannot be checked out'.format(remote, rev)
                        )
                        log.error(msg.format(name))
                        return _fail(ret, msg.format('Repository'), comments)
                else:
                    if remote_rev_type == 'tag' \
                            and rev not in __salt__['git.list_tags'](
                                target, user=user):
                        return _fail(
                            ret,
                            'Revision \'{0}\' does not exist in clone'
                            .format(rev),
                            comments
                        )

                    if branch is not None:
                        if branch not in \
                                __salt__['git.list_branches'](target,
                                                              user=user):
                            if rev == 'HEAD':
                                checkout_rev = remote_rev
                            else:
                                checkout_rev = desired_upstream \
                                    if desired_upstream \
                                    else rev
                            __salt__['git.checkout'](target,
                                                     checkout_rev,
                                                     opts=['-b', branch],
                                                     user=user)
                            comments.append(
                                'Branch \'{0}\' checked out, with {1} '
                                'as a starting point'.format(
                                    branch,
                                    remote_loc
                                )
                            )

                    local_rev, local_branch = \
                        _get_local_rev_and_branch(target, user)

                    if not _revs_equal(local_rev, remote_rev, remote_rev_type):
                        __salt__['git.reset'](
                            target,
                            opts=['--hard', remote_rev],
                            user=user
                        )
                        comments.append(
                            'Repository was reset to {0}'.format(remote_loc)
                        )

                    try:
                        upstream = __salt__['git.rev_parse'](
                            target,
                            local_branch + '@{upstream}',
                            opts=['--abbrev-ref'],
                            user=user,
                            ignore_retcode=True)
                    except CommandExecutionError:
                        upstream = False

                    if not upstream and desired_upstream:
                        upstream_action = (
                            'Tracking branch was set to {0}'.format(
                                desired_upstream
                            )
                        )
                        branch_opts = _get_branch_opts(
                            branch,
                            local_branch,
                            __salt__['git.list_branches'](target, user=user),
                            desired_upstream,
                            git_ver)
                    elif upstream and desired_upstream is False:
                        # If the remote_rev is a tag or SHA1, and there is an
                        # upstream tracking branch, we will unset it. However,
                        # we can only do this if the git version is 1.8.0 or
                        # newer, as the --unset-upstream option was not added
                        # until that version.
                        if git_ver >= _LooseVersion('1.8.0'):
                            upstream_action = 'Tracking branch was unset'
                            branch_opts = ['--unset-upstream']
                        else:
                            branch_opts = None
                    elif desired_upstream and upstream != desired_upstream:
                        upstream_action = (
                            'Tracking branch was updated to {0}'.format(
                                desired_upstream
                            )
                        )
                        branch_opts = _get_branch_opts(
                            branch,
                            local_branch,
                            __salt__['git.list_branches'](target, user=user),
                            desired_upstream,
                            git_ver)
                    else:
                        branch_opts = None

                    if branch_opts is not None:
                        __salt__['git.branch'](
                            target,
                            opts=branch_opts,
                            user=user)
                        comments.append(upstream_action)

            if submodules and remote_rev:
                try:
                    __salt__['git.submodule'](target,
                                              'update',
                                              opts=['--init', '--recursive'],
                                              user=user,
                                              identity=identity)
                except CommandExecutionError as exc:
                    return _failed_submodule_update(ret, exc, comments)

            try:
                new_rev = __salt__['git.revision'](
                    cwd=target,
                    user=user,
                    ignore_retcode=True)
            except CommandExecutionError:
                new_rev = None

        except Exception as exc:
            log.error(
                'Unexpected exception in git.latest state',
                exc_info=True
            )
            if isinstance(exc, CommandExecutionError):
                msg = _strip_exc(exc)
            else:
                msg = str(exc)
            return _fail(ret, msg, comments)

        msg = _format_comments(comments)
        log.info(msg)
        ret['comment'] = msg
        if new_rev is not None:
            ret['changes']['revision'] = {'old': None, 'new': new_rev}
    return ret
dataset/ETHPy150Open saltstack/salt/salt/states/git.py/latest