PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/states/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/states/git.py |
""" States to manage git repositories and git configuration .. important:: Before using git over ssh, make sure your remote host fingerprint exists in your ``~/.ssh/known_hosts`` file. .. versionchanged:: 2015.8.8 This state module now requires git 1.6.5 (released 10 October 2009) or newer. """ import errno import logging import os import re import string import salt.utils.args import salt.utils.files import salt.utils.url import salt.utils.versions from salt.exceptions import CommandExecutionError from salt.utils.versions import Version log = logging.getLogger(__name__) def __virtual__(): """ Only load if git is available """ if "git.version" not in __salt__: return (False, "git module could not be loaded") git_ver = Version(__salt__["git.version"](versioninfo=False)) return git_ver >= Version("1.6.5") def _revs_equal(rev1, rev2, rev_type): """ Shorthand helper function for comparing SHA1s. If rev_type == 'sha1' then the comparison will be done using str.startwith() to allow short SHA1s to compare successfully. NOTE: This means that rev2 must be the short rev. """ if (rev1 is None and rev2 is not None) or (rev2 is None and rev1 is not None): return False elif rev1 is rev2 is None: return True elif rev_type == "sha1": return rev1.startswith(rev2) else: return rev1 == rev2 def _short_sha(sha1): return sha1[:7] if sha1 is not None else None def _format_comments(comments): """ Return a joined list """ ret = ". ".join(comments) if len(comments) > 1: ret += "." return ret def _need_branch_change(branch, local_branch): """ Short hand for telling when a new branch is needed """ return branch is not None and branch != local_branch def _get_branch_opts( branch, local_branch, all_local_branches, desired_upstream, git_ver=None ): """ DRY helper to build list of opts for git.branch, for the purposes of setting upstream tracking branch """ if branch is not None and branch not in all_local_branches: # We won't be setting upstream because the act of checking out a new # branch will set upstream for us return None if git_ver is None: git_ver = Version(__salt__["git.version"](versioninfo=False)) ret = [] if git_ver >= Version("1.8.0"): ret.extend(["--set-upstream-to", desired_upstream]) else: ret.append("--set-upstream") # --set-upstream does not assume the current branch, so we have to # tell it which branch we'll be using ret.append(local_branch if branch is None else branch) ret.append(desired_upstream) return ret def _get_local_rev_and_branch(target, user, password, output_encoding=None): """ Return the local revision for before/after comparisons """ log.info("Checking local revision for %s", target) try: local_rev = __salt__["git.revision"]( target, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError: log.info("No local revision for %s", target) local_rev = None log.info("Checking local branch for %s", target) try: local_branch = __salt__["git.current_branch"]( target, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError: log.info("No local branch for %s", target) local_branch = None return local_rev, local_branch def _strip_exc(exc): """ Strip the actual command that was run from exc.strerror to leave just the error message """ return re.sub(r'^Command [\'"].+[\'"] failed: ', "", exc.strerror) def _uptodate(ret, target, comments=None, local_changes=False): ret["comment"] = f"Repository {target} is up-to-date" if local_changes: ret["comment"] += ( ", but with uncommitted changes. Set 'force_reset' to True to " "purge uncommitted changes." ) if comments: # Shouldn't be making any changes if the repo was up to date, but # report on them so we are alerted to potential problems with our # logic. ret["comment"] += "\n\nChanges {}made: {}".format( "that would be " if __opts__["test"] else "", _format_comments(comments) ) return ret def _neutral_test(ret, comment): ret["result"] = None ret["comment"] = comment return ret def _fail(ret, msg, comments=None): ret["result"] = False if comments: msg += "\n\nChanges already made: " + _format_comments(comments) ret["comment"] = msg return ret def _already_cloned(ret, target, branch=None, comments=None): ret["result"] = True ret["comment"] = "Repository already exists at {}{}".format( target, f" and is checked out to branch '{branch}'" if branch else "" ) if comments: ret["comment"] += "\n\nChanges {}made: {}".format( "that would be " if __opts__["test"] else "", _format_comments(comments) ) return ret def _failed_fetch(ret, exc, comments=None): msg = ( "Fetch failed. Set 'force_fetch' to True to force the fetch if the " "failure was due to not being able to fast-forward. Output of the fetch " "command follows:\n\n{}".format(_strip_exc(exc)) ) return _fail(ret, msg, comments) def _failed_submodule_update(ret, exc, comments=None): msg = "Failed to update submodules: " + _strip_exc(exc) return _fail(ret, msg, comments) def _not_fast_forward( ret, rev, pre, post, branch, local_branch, default_branch, local_changes, comments ): branch_msg = "" if branch is None: if rev != "HEAD": if local_branch != rev: branch_msg = ( " The desired rev ({0}) differs from the name of the " "local branch ({1}), if the desired rev is a branch name " "then a forced update could possibly be avoided by " "setting the 'branch' argument to '{0}' instead.".format( rev, local_branch ) ) else: if default_branch is not None and local_branch != default_branch: branch_msg = ( " The default remote branch ({0}) differs from the " "local branch ({1}). This could be caused by changing the " "default remote branch, or if the local branch was " "manually changed. Rather than forcing an update, it " "may be advisable to set the 'branch' argument to " "'{0}' instead. To ensure that this state follows the " "'{0}' branch instead of the remote HEAD, set the " "'rev' argument to '{0}'.".format(default_branch, local_branch) ) pre = _short_sha(pre) post = _short_sha(post) return _fail( ret, "Repository would be updated {}{}, but {}. Set 'force_reset' to " "True{} to force this update{}.{}".format( f"from {pre} to {post}" if local_changes and pre != post else f"to {post}", ( f" (after checking out local branch '{branch}')" if _need_branch_change(branch, local_branch) else "" ), ( "this is not a fast-forward merge" if not local_changes else "there are uncommitted changes" ), " (or 'remote-changes')" if local_changes else "", " and discard these changes" if local_changes else "", branch_msg, ), comments, ) def latest( name, target, rev="HEAD", branch=None, user=None, password=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, sync_tags=True, depth=None, identity=None, https_user=None, https_pass=None, refspec_branch="*", refspec_tag="*", output_encoding=None, **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`` .. note:: From the `Git documentation`_, there are two URL formats supported for SSH authentication. The below two examples are equivalent: .. code-block:: text # ssh:// URL ssh://user@server/project.git # SCP-like syntax user@server:project.git A common mistake is to use an ``ssh://`` URL, but with a colon after the domain instead of a slash. This is invalid syntax in Git, and will therefore not work in Salt. When in doubt, confirm that a ``git clone`` works for the URL before using it in Salt. It has been reported by some users that SCP-like syntax is incompatible with git repos hosted on `Atlassian Stash/BitBucket Server`_. In these cases, it may be necessary to use ``ssh://`` URLs for SSH authentication. .. _`Git documentation`: https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#The-SSH-Protocol .. _`Atlassian Stash/BitBucket Server`: https://www.atlassian.com/software/bitbucket/server 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 local 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. .. versionadded:: 2015.8.0 .. note:: If this argument is not specified, this means that Salt will not change the local branch if the repository is reset to another branch/tag/SHA1. For example, assume that the following state was run initially: .. code-block:: yaml foo_app: git.latest: - name: https://mydomain.tld/apps/foo.git - target: /var/www/foo - user: www This would have cloned the HEAD of that repo (since a ``rev`` wasn't specified), and because ``branch`` is not specified, the branch in the local clone at ``/var/www/foo`` would be whatever the default branch is on the remote repository (usually ``master``, but not always). Now, assume that it becomes necessary to switch this checkout to the ``dev`` branch. This would require ``rev`` to be set, and probably would also require ``force_reset`` to be enabled: .. code-block:: yaml foo_app: git.latest: - name: https://mydomain.tld/apps/foo.git - target: /var/www/foo - user: www - rev: dev - force_reset: True The result of this state would be to perform a hard-reset to ``origin/dev``. Since ``branch`` was not specified though, while ``/var/www/foo`` would reflect the contents of the remote repo's ``dev`` branch, the local branch would still remain whatever it was when it was cloned. To make the local branch match the remote one, set ``branch`` as well, like so: .. code-block:: yaml foo_app: git.latest: - name: https://mydomain.tld/apps/foo.git - target: /var/www/foo - user: www - rev: dev - branch: dev - force_reset: True This may seem redundant, but Salt tries to support a wide variety of use cases, and doing it this way allows for the use case where the local branch doesn't need to be strictly managed. user Local system user under which to run git commands. By default, commands are run by the user under which the minion is running. .. note:: This is not to be confused with the username for http(s)/SSH authentication. .. versionadded:: 0.17.0 password Windows only. Required when specifying ``user``. This parameter will be ignored on non-Windows platforms. .. versionadded:: 2016.3.4 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_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. .. versionchanged:: 2019.2.0 This option can now be set to ``remote-changes``, which will instruct Salt not to discard local changes if the repo is up-to-date with the remote repository. 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. 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. sync_tags : True If ``True``, then Salt will delete tags which exist in the local clone but are not found on the remote repository. .. versionadded:: 2018.3.4 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 or tag. This is not compatible with revision IDs. .. versionchanged:: 2019.2.0 This option now supports tags as well as branches, on Git 1.8.0 and newer. 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. .. versionchanged:: 2016.3.0 Key can now be specified as a SaltStack fileserver URL (e.g. ``salt://path/to/identity_file``). 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 refspec_branch : * A glob expression defining which branches to retrieve when fetching. See `git-fetch(1)`_ for more information on how refspecs work. .. versionadded:: 2017.7.0 refspec_tag : * A glob expression defining which tags to retrieve when fetching. See `git-fetch(1)`_ for more information on how refspecs work. .. versionadded:: 2017.7.0 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-fetch(1)`: http://git-scm.com/docs/git-fetch .. 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`` argument. 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: 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: 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 git-website-prod: git.latest: - name: 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.args.clean_kwargs(**kwargs) if kwargs: return _fail(ret, salt.utils.args.invalid_kwargs(kwargs, raise_exc=False)) 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, f"'{rev}' is not a valid value for the 'rev' argument") if force_reset not in (True, False, "remote-changes"): return _fail( ret, "'force_reset' must be one of True, False, or 'remote-changes'" ) # Ensure that certain arguments are strings to ensure that comparisons work if not isinstance(rev, str): rev = str(rev) if target is not None: if not isinstance(target, str): target = str(target) if not os.path.isabs(target): return _fail(ret, f"target '{target}' is not an absolute path") if branch is not None and not isinstance(branch, str): branch = str(branch) if user is not None and not isinstance(user, str): user = str(user) if password is not None and not isinstance(password, str): password = str(password) if remote is not None and not isinstance(remote, str): remote = str(remote) if identity is not None: if isinstance(identity, str): identity = [identity] elif not isinstance(identity, list): return _fail(ret, "identity must be either a list or a string") identity = [os.path.expanduser(x) for x in identity] for ident_path in identity: if "salt://" in ident_path: try: ident_path = __salt__["cp.cache_file"](ident_path, __env__) except OSError as exc: log.exception("Failed to cache %s", ident_path) return _fail(ret, f"identity '{ident_path}' does not exist.") if not os.path.isabs(ident_path): return _fail(ret, f"identity '{ident_path}' is not an absolute path") if https_user is not None and not isinstance(https_user, str): https_user = str(https_user) if https_pass is not None and not isinstance(https_pass, str): https_pass = str(https_pass) # Check for lfs filter settings, and setup lfs_opts accordingly. These opts # will be passed where appropriate to ensure that these commands are # authenticated and that the git LFS plugin can download files. use_lfs = bool( __salt__["git.config_get_regexp"]( r"filter\.lfs\.", **{"global": True, "ignore_retcode": True} ) ) lfs_opts = {"identity": identity} if use_lfs else {} if os.path.isfile(target): return _fail( ret, f"Target '{target}' exists and is a regular file, cannot proceed", ) 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, str(exc)) 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, "password": password} if "shell" in __grains__: run_check_cmd_kwargs["shell"] = __grains__["shell"] refspecs = ( [ "refs/heads/{0}:refs/remotes/{1}/{0}".format(refspec_branch, remote), "+refs/tags/{0}:refs/tags/{0}".format(refspec_tag), ] if fetch_tags else [] ) log.info("Checking remote revision for %s", name) try: all_remote_refs = __salt__["git.remote_refs"]( name, heads=False, tags=False, user=user, password=password, identity=identity, https_user=https_user, https_pass=https_pass, ignore_retcode=False, saltenv=__env__, output_encoding=output_encoding, ) except CommandExecutionError as exc: return _fail(ret, f"Failed to check remote refs: {_strip_exc(exc)}") if "HEAD" in all_remote_refs: head_rev = all_remote_refs["HEAD"] for refname, refsha in all_remote_refs.items(): if refname.startswith("refs/heads/"): if refsha == head_rev: default_branch = refname.partition("refs/heads/")[-1] break else: default_branch = None else: head_rev = None default_branch = None desired_upstream = False if bare: remote_rev = None remote_rev_type = None else: if rev == "HEAD": if head_rev is not None: remote_rev = head_rev # 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 + "^{}"] remote_rev_type = "tag" elif "refs/tags/" + rev in all_remote_refs: # Non-annotated tag remote_rev = all_remote_refs["refs/tags/" + rev] 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 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 = "{} ({})".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 depth is not None and remote_rev_type not in ("branch", "tag"): return _fail( ret, "When 'depth' is used, 'rev' must be set to the name of a " "branch or tag on the remote repository", ) 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, f"No revision matching '{rev}' exists in the remote repository", ) git_ver = Version(__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, user=user, password=password, output_encoding=output_encoding ): # Target directory is a git repository or git worktree try: all_local_branches = __salt__["git.list_branches"]( target, user=user, password=password, output_encoding=output_encoding ) all_local_tags = set( __salt__["git.list_tags"]( target, user=user, password=password, output_encoding=output_encoding, ) ) local_rev, local_branch = _get_local_rev_and_branch( target, user, password, output_encoding ) 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, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError as exc: return _fail( ret, "Unable to get position of local branch '{}': {}".format( branch, _strip_exc(exc) ), comments, ) remotes = __salt__["git.remotes"]( target, user=user, password=password, redact_auth=False, output_encoding=output_encoding, ) revs_match = _revs_equal(local_rev, remote_rev, remote_rev_type) try: # If not a bare repo, check `git diff HEAD` to determine if # there are local changes. local_changes = bool( not bare and __salt__["git.diff"]( target, "HEAD", user=user, password=password, output_encoding=output_encoding, ) ) except CommandExecutionError: # No need to capture the error and log it, the _git_run() # helper in the git execution module will have already logged # the output from the command. log.warning( "git.latest: Unable to determine if %s has local changes", target ) local_changes = False if local_changes and revs_match: if force_reset is True: msg = ( "{} is up-to-date, but with uncommitted changes. " "Since 'force_reset' is set to True, these local " "changes would be reset. To only reset when there are " "changes in the remote repository, set " "'force_reset' to 'remote-changes'.".format(target) ) if __opts__["test"]: ret["changes"]["forced update"] = True if comments: msg += _format_comments(comments) return _neutral_test(ret, msg) log.debug(msg.replace("would", "will")) else: log.debug( "%s up-to-date, but with uncommitted changes. Since " "'force_reset' is set to %s, no changes will be " "made.", target, force_reset, ) return _uptodate( ret, target, _format_comments(comments), local_changes ) 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}", user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) 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, password=password, ignore_retcode=True, output_encoding=output_encoding, ) 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, password=password, ignore_retcode=True, output_encoding=output_encoding, ) 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, "'{}' is a tag, but the remote " "SHA1 for this tag ({}) doesn't " "match the local SHA1 ({}). 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 fast_forward is not boolean, then we don't yet know if this # will be a fast forward or not, because a fetch is required. fast_forward = ( False if (local_changes and force_reset != "remote-changes") else None ) if has_remote_rev: if (not revs_match and not update_head) and ( branch is None or branch == local_branch ): # pylint: disable=used-before-assignment ret["comment"] = ( "{} is already present and local HEAD ({}) does not " "match, but update_head=False. HEAD has not been " "updated locally.".format( remote_loc.capitalize() if rev == "HEAD" else remote_loc, local_rev[:7], ) ) return ret # No need to check if this is a fast_forward if we already know # that it won't be (due to local changes). if fast_forward is not False: 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, password=password, ignore_retcode=True, output_encoding=output_encoding, ) if fast_forward is False: if force_reset is False: return _not_fast_forward( ret, rev, base_rev, remote_rev, branch, local_branch, default_branch, local_changes, 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, password=password, ignore_retcode=True, output_encoding=output_encoding, ) 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 '%s' not found in git checkout at %s", remote, target) fetch_url = None if remote_rev is not None and desired_fetch_url != fetch_url: if __opts__["test"]: actions = [ "Remote '{}' would be changed from {} to {}".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 {} to {}".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, password=password, https_user=https_user, https_pass=https_pass, output_encoding=output_encoding, ) if fetch_url is None: comments.append(f"Remote '{remote}' set to {redacted_fetch_url}") ret["changes"]["new"] = name + " => " + remote else: comments.append( "Remote '{}' changed from {} to {}".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(f"Remote '{remote}' would be fetched") 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 '{}' would be checked " "out, with {} as a starting " "point".format(branch, remote_loc) ) if desired_upstream: actions.append( "Tracking branch would be set to {}".format( desired_upstream ) ) else: actions.append( "Branch '{}' would be checked out and {} to {}".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 " "{} to {}".format( _short_sha(local_rev), _short_sha(remote_rev), ) ) else: actions.append( "Repository would be {} from {} to {}".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 ({}) does not match {} 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 {}".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 {}".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 {}".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 >= Version("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 {}".format( desired_upstream ) branch_opts = _get_branch_opts( branch, local_branch, all_local_branches, desired_upstream, git_ver, ) else: branch_opts = None if branch_opts is not None and local_branch is None: return _fail( ret, "Cannot set/unset upstream tracking branch, local " "HEAD refers to nonexistent branch. This may have " "been caused by cloning a remote repository for which " "the default branch was renamed or deleted. If you " "are unable to fix the remote repository, you can " "work around this by setting the 'branch' argument " "(which will ensure that the named branch is created " "if it does not already exist).", comments, ) remote_tags = { x.replace("refs/tags/", "") for x in __salt__["git.ls_remote"]( cwd=target, remote=remote, opts="--tags", user=user, password=password, identity=identity, saltenv=__env__, ignore_retcode=True, output_encoding=output_encoding, ) if "^{}" not in x } if all_local_tags != remote_tags: has_remote_rev = False new_tags = remote_tags - all_local_tags deleted_tags = all_local_tags - remote_tags if new_tags: ret["changes"]["new_tags"] = new_tags if sync_tags and deleted_tags: # Delete the local copy of the tags to keep up with the # remote repository. for tag_name in deleted_tags: try: if not __opts__["test"]: __salt__["git.tag"]( target, tag_name, opts="-d", user=user, password=password, output_encoding=output_encoding, ) except CommandExecutionError as exc: ret.setdefault("warnings", []).append( "Failed to remove local tag '{}':\n\n{}\n\n".format( tag_name, exc ) ) else: ret["changes"].setdefault("deleted_tags", []).append( tag_name ) if ret["changes"].get("deleted_tags"): comments.append( "The following tags {} removed from the local " "checkout: {}".format( "would be" if __opts__["test"] else "were", ", ".join(ret["changes"]["deleted_tags"]), ) ) if not has_remote_rev: try: fetch_changes = __salt__["git.fetch"]( target, remote=remote, force=force_fetch, refspecs=refspecs, user=user, password=password, identity=identity, saltenv=__env__, output_encoding=output_encoding, ) except CommandExecutionError as exc: return _failed_fetch(ret, exc, comments) else: if fetch_changes: comments.append( f"{name} was fetched, resulting in updated refs" ) try: __salt__["git.rev_parse"]( target, remote_rev + "^{commit}", user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError as exc: return _fail( ret, "Fetch did not successfully retrieve rev '{}' " "from {}: {}".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 ({}) 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, password=password, output_encoding=output_encoding, ) if fast_forward is force_reset is False or ( fast_forward is True and local_changes and force_reset is False ): return _not_fast_forward( ret, rev, base_rev, remote_rev, branch, local_branch, default_branch, local_changes, comments, ) if _need_branch_change(branch, local_branch): if local_changes and not force_checkout: return _fail( ret, "Local branch '{}' 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, password=password, output_encoding=output_encoding, ) if "-b" in checkout_opts: comments.append( "New branch '{}' was checked out, with {} " "as a starting point".format(branch, remote_loc) ) else: comments.append(f"'{checkout_rev}' was checked out") if fast_forward is False: __salt__["git.reset"]( target, opts=["--hard", remote_rev], user=user, password=password, output_encoding=output_encoding, **lfs_opts, ) ret["changes"]["forced update"] = True if local_changes: comments.append("Uncommitted changes were discarded") comments.append(f"Repository was hard-reset to {remote_loc}") elif ( fast_forward is True and local_changes and force_reset is not False ): __salt__["git.discard_local_changes"]( target, user=user, password=password, output_encoding=output_encoding, ) comments.append("Uncommitted changes were discarded") if branch_opts is not None: __salt__["git.branch"]( target, opts=branch_opts, user=user, password=password, output_encoding=output_encoding, ) 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"], user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ): if git_ver >= Version("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=remote_rev, opts=merge_opts, user=user, password=password, output_encoding=output_encoding, **lfs_opts, ) comments.append( f"Repository was fast-forwarded to {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, password=password, output_encoding=output_encoding, **lfs_opts, ) comments.append(f"Repository was reset to {rev} (fast-forward)") # 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, password=password, identity=identity, saltenv=__env__, output_encoding=output_encoding, ) except CommandExecutionError as exc: return _failed_submodule_update(ret, exc, comments) elif bare: if __opts__["test"]: msg = f"Bare repository at {target} would be fetched" 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, password=password, identity=identity, saltenv=__env__, output_encoding=output_encoding, ) except CommandExecutionError as exc: return _failed_fetch(ret, exc, comments) else: comments.append( "Bare repository at {} was fetched{}".format( target, ", resulting in updated refs" if fetch_changes else "", ) ) try: new_rev = __salt__["git.revision"]( cwd=target, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError: new_rev = None except Exception as exc: # pylint: disable=broad-except 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 %s updated: %s => %s", 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): target_contents = os.listdir(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 %s to clone repository %s in its " "place (force_clone=True set in git.latest state)", target, name, ) removal_errors = {} for target_object in target_contents: target_path = os.path.join(target, target_object) try: salt.utils.files.rm_rf(target_path) except OSError as exc: if exc.errno != errno.ENOENT: removal_errors[target_path] = exc if removal_errors: err_strings = [f" {k}\n {v}" for k, v in removal_errors.items()] return _fail( ret, "Unable to remove\n{}".format("\n".join(err_strings)), comments, ) ret["changes"]["forced clone"] = True # Clone is required, but target dir exists and is non-empty. We # can't proceed. elif target_contents: return _fail( ret, "Target '{}' 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 %s is not found, 'git clone' is required", target) if __opts__["test"]: ret["changes"]["new"] = name + " => " + target return _neutral_test(ret, f"Repository {name} would be cloned to {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), "--branch", rev]) # 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, password=password, opts=clone_opts, identity=identity, https_user=https_user, https_pass=https_pass, saltenv=__env__, output_encoding=output_encoding, ) except CommandExecutionError as exc: msg = f"Clone failed: {_strip_exc(exc)}" return _fail(ret, msg, comments) ret["changes"]["new"] = name + " => " + target comments.append( "{} cloned to {}{}".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 = ( "%s was cloned but is empty, so {}/{} " "cannot be checked out".format(remote, rev) ) log.error(msg, name) # Disable check for string substitution return _fail(ret, msg % "Repository", comments) else: if remote_rev_type == "tag" and rev not in __salt__[ "git.list_tags" ]( target, user=user, password=password, output_encoding=output_encoding, ): return _fail( ret, f"Revision '{rev}' does not exist in clone", comments, ) if branch is not None: if branch not in __salt__["git.list_branches"]( target, user=user, password=password, output_encoding=output_encoding, ): 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, password=password, output_encoding=output_encoding, ) comments.append( "Branch '{}' checked out, with {} " "as a starting point".format(branch, remote_loc) ) local_rev, local_branch = _get_local_rev_and_branch( target, user, password, output_encoding=output_encoding ) if ( local_branch is None and remote_rev is not None and "HEAD" not in all_remote_refs ): return _fail( ret, "Remote HEAD refers to a ref that does not exist. " "This can happen when the default branch on the " "remote repository is renamed or deleted. If you " "are unable to fix the remote repository, you can " "work around this by setting the 'branch' argument " "(which will ensure that the named branch is created " "if it does not already exist).", comments, ) if not _revs_equal(local_rev, remote_rev, remote_rev_type): __salt__["git.reset"]( target, opts=["--hard", remote_rev], user=user, password=password, output_encoding=output_encoding, ) comments.append(f"Repository was reset to {remote_loc}") try: upstream = __salt__["git.rev_parse"]( target, local_branch + "@{upstream}", opts=["--abbrev-ref"], user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError: upstream = False if not upstream and desired_upstream: upstream_action = "Tracking branch was set to {}".format( desired_upstream ) branch_opts = _get_branch_opts( branch, local_branch, __salt__["git.list_branches"]( target, user=user, password=password, output_encoding=output_encoding, ), 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 >= Version("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 {}".format( desired_upstream ) branch_opts = _get_branch_opts( branch, local_branch, __salt__["git.list_branches"]( target, user=user, password=password, output_encoding=output_encoding, ), desired_upstream, git_ver, ) else: branch_opts = None if branch_opts is not None: __salt__["git.branch"]( target, opts=branch_opts, user=user, password=password, output_encoding=output_encoding, ) comments.append(upstream_action) if submodules and remote_rev: try: __salt__["git.submodule"]( target, "update", opts=["--init", "--recursive"], user=user, password=password, identity=identity, output_encoding=output_encoding, ) except CommandExecutionError as exc: return _failed_submodule_update(ret, exc, comments) try: new_rev = __salt__["git.revision"]( cwd=target, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError: new_rev = None except Exception as exc: # pylint: disable=broad-except 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 def present( name, force=False, bare=True, template=None, separate_git_dir=None, shared=None, user=None, password=None, output_encoding=None, ): """ Ensure that a repository exists in the given directory .. warning:: If the minion has Git 2.5 or later installed, ``name`` points to a worktree_, and ``force`` is set to ``True``, then the worktree will be deleted. This has been corrected in Salt 2015.8.0. name Path to the directory .. versionchanged:: 2015.8.0 This path must now be absolute force : False If ``True``, and if ``name`` points to an existing directory which does not contain a git repository, then the contents of that directory will be recursively removed and a new repository will be initialized in its place. bare : True If ``True``, and a repository must be initialized, then the repository will be a bare repository. .. note:: This differs from the default behavior of :py:func:`git.init <salt.modules.git.init>`, make sure to set this value to ``False`` if a bare repo is not desired. template If a new repository is initialized, this argument will specify an alternate template directory. .. versionadded:: 2015.8.0 separate_git_dir If a new repository is initialized, this argument will specify an alternate ``$GIT_DIR`` .. versionadded:: 2015.8.0 shared Set sharing permissions on git repo. See `git-init(1)`_ for more details. .. versionadded:: 2015.5.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 password Windows only. Required when specifying ``user``. This parameter will be ignored on non-Windows platforms. .. versionadded:: 2016.3.4 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-init(1)`: http://git-scm.com/docs/git-init .. _`worktree`: http://git-scm.com/docs/git-worktree """ ret = {"name": name, "result": True, "comment": "", "changes": {}} # If the named directory is a git repo return True if os.path.isdir(name): if bare and os.path.isfile(os.path.join(name, "HEAD")): return ret elif not bare and ( os.path.isdir(os.path.join(name, ".git")) or __salt__["git.is_worktree"]( name, user=user, password=password, output_encoding=output_encoding ) ): return ret # Directory exists and is not a git repo, if force is set destroy the # directory and recreate, otherwise throw an error elif force: # Directory exists, and the ``force`` option is enabled, so we need # to clear out its contents to proceed. if __opts__["test"]: ret["changes"]["new"] = name ret["changes"]["forced init"] = True return _neutral_test( ret, "Target directory {0} exists. Since force=True, the " "contents of {0} would be deleted, and a {1}repository " "would be initialized in its place.".format( name, "bare " if bare else "" ), ) log.debug( "Removing contents of %s to initialize %srepository in its " "place (force=True set in git.present state)", name, "bare " if bare else "", ) try: if os.path.islink(name): os.unlink(name) else: salt.utils.files.rm_rf(name) except OSError as exc: return _fail(ret, f"Unable to remove {name}: {exc}") else: ret["changes"]["forced init"] = True elif os.listdir(name): return _fail( ret, "Target '{}' exists, is non-empty, and is not a git " "repository. Set the 'force' option to True to remove " "this directory's contents and proceed with initializing a " "repository".format(name), ) # Run test is set if __opts__["test"]: ret["changes"]["new"] = name return _neutral_test( ret, "New {}repository would be created".format("bare " if bare else "") ) __salt__["git.init"]( cwd=name, bare=bare, template=template, separate_git_dir=separate_git_dir, shared=shared, user=user, password=password, output_encoding=output_encoding, ) actions = ["Initialized {}repository in {}".format("bare " if bare else "", name)] if template: actions.append(f"Template directory set to {template}") if separate_git_dir: actions.append(f"Gitdir set to {separate_git_dir}") message = ". ".join(actions) if len(actions) > 1: message += "." log.info(message) ret["changes"]["new"] = name ret["comment"] = message return ret def detached( name, rev, target, remote="origin", user=None, password=None, force_clone=False, force_checkout=False, fetch_remote=True, hard_reset=False, submodules=False, identity=None, https_user=None, https_pass=None, output_encoding=None, **kwargs, ): """ .. versionadded:: 2016.3.0 Make sure a repository is cloned to the given target directory and is a detached HEAD checkout of the commit ID resolved from ``rev``. name Address of the remote repository. rev The branch, tag, or commit ID to checkout after clone. If a branch or tag is specified it will be resolved to a commit ID and checked out. target Name of the target directory where repository is about to be cloned. 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. user User under which to run git commands. By default, commands are 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 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_checkout : False When checking out the revision ID, the state will fail if there are unwritten changes. Set this argument to ``True`` to discard unwritten changes when checking out. fetch_remote : True If ``False`` a fetch will not be performed and only local refs will be reachable. hard_reset : False If ``True`` a hard reset will be performed before the checkout and any uncommitted modifications to the working directory will be discarded. Untracked files will remain in place. .. note:: Changes resulting from a hard reset will not trigger requisites. submodules : False Update submodules identity A path on the minion (or a SaltStack fileserver URL, e.g. ``salt://path/to/identity_file``) to a private key to use for SSH authentication. https_user HTTP Basic Auth username for HTTPS (only) clones https_pass HTTP Basic Auth password for HTTPS (only) clones 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 """ ret = {"name": name, "result": True, "comment": "", "changes": {}} kwargs = salt.utils.args.clean_kwargs(**kwargs) if kwargs: return _fail(ret, salt.utils.args.invalid_kwargs(kwargs, raise_exc=False)) if not rev: return _fail(ret, f"'{rev}' is not a valid value for the 'rev' argument") if not target: return _fail(ret, f"'{rev}' is not a valid value for the 'target' argument") # Ensure that certain arguments are strings to ensure that comparisons work if not isinstance(rev, str): rev = str(rev) if target is not None: if not isinstance(target, str): target = str(target) if not os.path.isabs(target): return _fail(ret, f"Target '{target}' is not an absolute path") if user is not None and not isinstance(user, str): user = str(user) if remote is not None and not isinstance(remote, str): remote = str(remote) if identity is not None: if isinstance(identity, str): identity = [identity] elif not isinstance(identity, list): return _fail(ret, "Identity must be either a list or a string") identity = [os.path.expanduser(x) for x in identity] for ident_path in identity: if "salt://" in ident_path: try: ident_path = __salt__["cp.cache_file"](ident_path) except OSError as exc: log.error("Failed to cache %s: %s", ident_path, exc) return _fail(ret, f"Identity '{ident_path}' does not exist.") if not os.path.isabs(ident_path): return _fail(ret, f"Identity '{ident_path}' is not an absolute path") if https_user is not None and not isinstance(https_user, str): https_user = str(https_user) if https_pass is not None and not isinstance(https_pass, str): https_pass = str(https_pass) if os.path.isfile(target): return _fail( ret, f"Target '{target}' exists and is a regular file, cannot proceed", ) 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, str(exc)) redacted_fetch_url = salt.utils.url.redact_http_basic_auth(desired_fetch_url) # Determine if supplied ref is a hash remote_rev_type = "ref" if len(rev) <= 40 and all(x in string.hexdigits for x in rev): rev = rev.lower() remote_rev_type = "hash" comments = [] hash_exists_locally = False local_commit_id = None gitdir = os.path.join(target, ".git") if os.path.isdir(gitdir) or __salt__["git.is_worktree"]( target, user=user, password=password, output_encoding=output_encoding ): # Target directory is a git repository or git worktree local_commit_id = _get_local_rev_and_branch( target, user, password, output_encoding=output_encoding )[0] if remote_rev_type == "hash": try: __salt__["git.describe"]( target, rev, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError: hash_exists_locally = False else: # The rev is a hash and it exists locally so skip to checkout hash_exists_locally = True else: # Check that remote is present and set to correct url remotes = __salt__["git.remotes"]( target, user=user, password=password, redact_auth=False, output_encoding=output_encoding, ) if remote in remotes and name in remotes[remote]["fetch"]: pass else: # The fetch_url for the desired remote does not match the # specified URL (or the remote does not exist), so set the # remote URL. current_fetch_url = None if remote in remotes: current_fetch_url = remotes[remote]["fetch"] if __opts__["test"]: return _neutral_test(ret, f"Remote {remote} would be set to {name}") __salt__["git.remote_set"]( target, url=name, remote=remote, user=user, password=password, https_user=https_user, https_pass=https_pass, output_encoding=output_encoding, ) comments.append( "Remote {} updated from '{}' to '{}'".format( remote, current_fetch_url, name ) ) else: # Clone repository if os.path.isdir(target): target_contents = os.listdir(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"]: 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 %s to clone repository %s in its " "place (force_clone=True set in git.detached state)", target, name, ) removal_errors = {} for target_object in target_contents: target_path = os.path.join(target, target_object) try: salt.utils.files.rm_rf(target_path) except OSError as exc: if exc.errno != errno.ENOENT: removal_errors[target_path] = exc if removal_errors: err_strings = [f" {k}\n {v}" for k, v in removal_errors.items()] return _fail( ret, "Unable to remove\n{}".format("\n".join(err_strings)), comments, ) ret["changes"]["forced clone"] = True elif target_contents: # Clone is required, but target dir exists and is non-empty. We # can't proceed. return _fail( ret, "Target '{}' 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 %s is not found, 'git clone' is required", target) if __opts__["test"]: return _neutral_test(ret, f"Repository {name} would be cloned to {target}") try: clone_opts = ["--no-checkout"] if remote != "origin": clone_opts.extend(["--origin", remote]) __salt__["git.clone"]( target, name, user=user, password=password, opts=clone_opts, identity=identity, https_user=https_user, https_pass=https_pass, saltenv=__env__, output_encoding=output_encoding, ) comments.append(f"{name} cloned to {target}") except Exception as exc: # pylint: disable=broad-except log.error("Unexpected exception in git.detached state", exc_info=True) if isinstance(exc, CommandExecutionError): msg = _strip_exc(exc) else: msg = str(exc) return _fail(ret, msg, comments) # Repository exists and is ready for fetch/checkout refspecs = [ f"refs/heads/*:refs/remotes/{remote}/*", "+refs/tags/*:refs/tags/*", ] if hash_exists_locally or fetch_remote is False: pass else: # Fetch refs from remote if __opts__["test"]: return _neutral_test(ret, f"Repository remote {remote} would be fetched") try: fetch_changes = __salt__["git.fetch"]( target, remote=remote, force=True, refspecs=refspecs, user=user, password=password, identity=identity, saltenv=__env__, output_encoding=output_encoding, ) except CommandExecutionError as exc: msg = "Fetch failed" msg += ":\n\n" + str(exc) return _fail(ret, msg, comments) else: if fetch_changes: comments.append( f"Remote {remote} was fetched, resulting in updated refs" ) # get refs and checkout checkout_commit_id = "" if remote_rev_type == "hash": if __salt__["git.describe"]( target, rev, user=user, password=password, output_encoding=output_encoding ): checkout_commit_id = rev else: return _fail(ret, f"Revision '{rev}' does not exist") else: try: all_remote_refs = __salt__["git.remote_refs"]( target, user=user, password=password, identity=identity, https_user=https_user, https_pass=https_pass, ignore_retcode=False, output_encoding=output_encoding, ) if "refs/remotes/" + remote + "/" + rev in all_remote_refs: checkout_commit_id = all_remote_refs[ "refs/remotes/" + remote + "/" + rev ] elif "refs/tags/" + rev in all_remote_refs: checkout_commit_id = all_remote_refs["refs/tags/" + rev] else: return _fail(ret, f"Revision '{rev}' does not exist") except CommandExecutionError as exc: return _fail(ret, f"Failed to list refs for {remote}: {_strip_exc(exc)}") if hard_reset: if __opts__["test"]: return _neutral_test( ret, f"Hard reset to HEAD would be performed on {target}" ) __salt__["git.reset"]( target, opts=["--hard", "HEAD"], user=user, password=password, output_encoding=output_encoding, ) comments.append("Repository was reset to HEAD before checking out revision") # TODO: implement clean function for git module and add clean flag if checkout_commit_id == local_commit_id: new_rev = None else: if __opts__["test"]: ret["changes"]["HEAD"] = {"old": local_commit_id, "new": checkout_commit_id} return _neutral_test( ret, "Commit ID {} would be checked out at {}".format( checkout_commit_id, target ), ) __salt__["git.checkout"]( target, checkout_commit_id, force=force_checkout, user=user, password=password, output_encoding=output_encoding, ) comments.append(f"Commit ID {checkout_commit_id} was checked out at {target}") try: new_rev = __salt__["git.revision"]( cwd=target, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError: new_rev = None if submodules: __salt__["git.submodule"]( target, "update", opts=["--init", "--recursive"], user=user, password=password, identity=identity, output_encoding=output_encoding, ) comments.append("Submodules were updated") if new_rev is not None: ret["changes"]["HEAD"] = {"old": local_commit_id, "new": new_rev} else: comments.append("Already checked out at correct revision") msg = _format_comments(comments) log.info(msg) ret["comment"] = msg return ret def cloned( name, target, branch=None, user=None, password=None, identity=None, https_user=None, https_pass=None, output_encoding=None, ): """ .. versionadded:: 2018.3.3,2019.2.0 Ensure that a repository has been cloned to the specified target directory. If not, clone that repository. No fetches will be performed once cloned. name Address of the remote repository target Name of the target directory where repository should be cloned branch Remote branch to check out. If unspecified, the default branch (i.e. the one to the remote HEAD points) will be checked out. .. note:: The local branch name will match the remote branch name. If the branch name is changed, then that branch will be checked out locally, but keep in mind that remote repository will not be fetched. If your use case requires that you keep the clone up to date with the remote repository, then consider using :py:func:`git.latest <salt.states.git.latest>`. user User under which to run git commands. By default, commands are 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. identity Path to a private key to use for ssh URLs. Works the same way as in :py:func:`git.latest <salt.states.git.latest>`, see that state's documentation for more information. https_user HTTP Basic Auth username for HTTPS (only) clones https_pass HTTP Basic Auth password for HTTPS (only) clones 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. """ ret = {"name": name, "result": False, "comment": "", "changes": {}} if target is None: ret["comment"] = "'target' argument is required" return ret elif not isinstance(target, str): target = str(target) if not os.path.isabs(target): ret["comment"] = "'target' path must be absolute" return ret if branch is not None: if not isinstance(branch, str): branch = str(branch) if not branch: ret["comment"] = "Invalid 'branch' argument" return ret if not os.path.exists(target): need_clone = True else: try: __salt__["git.status"]( target, user=user, password=password, output_encoding=output_encoding ) except Exception as exc: # pylint: disable=broad-except ret["comment"] = str(exc) return ret else: need_clone = False comments = [] def _clone_changes(ret): ret["changes"]["new"] = name + " => " + target def _branch_changes(ret, old, new): ret["changes"]["branch"] = {"old": old, "new": new} if need_clone: if __opts__["test"]: _clone_changes(ret) comment = "{} would be cloned to {}{}".format( name, target, f" with branch '{branch}'" if branch is not None else "", ) return _neutral_test(ret, comment) clone_opts = ["--branch", branch] if branch is not None else None try: __salt__["git.clone"]( target, name, opts=clone_opts, user=user, password=password, identity=identity, https_user=https_user, https_pass=https_pass, output_encoding=output_encoding, ) except CommandExecutionError as exc: msg = f"Clone failed: {_strip_exc(exc)}" return _fail(ret, msg, comments) comments.append( "{} cloned to {}{}".format( name, target, f" with branch '{branch}'" if branch is not None else "", ) ) _clone_changes(ret) ret["comment"] = _format_comments(comments) ret["result"] = True return ret else: if branch is None: return _already_cloned(ret, target, branch, comments) else: current_branch = __salt__["git.current_branch"]( target, user=user, password=password, output_encoding=output_encoding ) if current_branch == branch: return _already_cloned(ret, target, branch, comments) else: if __opts__["test"]: _branch_changes(ret, current_branch, branch) return _neutral_test(ret, f"Branch would be changed to '{branch}'") try: __salt__["git.rev_parse"]( target, rev=branch, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, ) except CommandExecutionError: # Local head does not exist, so we need to check out a new # branch at the remote rev checkout_rev = "/".join(("origin", branch)) checkout_opts = ["-b", branch] else: # Local head exists, so we just need to check it out checkout_rev = branch checkout_opts = None try: __salt__["git.checkout"]( target, rev=checkout_rev, opts=checkout_opts, user=user, password=password, output_encoding=output_encoding, ) except CommandExecutionError as exc: msg = f"Failed to change branch to '{branch}': {exc}" return _fail(ret, msg, comments) else: comments.append(f"Branch changed to '{branch}'") _branch_changes(ret, current_branch, branch) ret["comment"] = _format_comments(comments) ret["result"] = True return ret def config_unset( name, value_regex=None, repo=None, user=None, password=None, output_encoding=None, **kwargs, ): r""" .. versionadded:: 2015.8.0 Ensure that the named config key is not present name The name of the configuration key to unset. This value can be a regex, but the regex must match the entire key name. For example, ``foo\.`` would not match all keys in the ``foo`` section, it would be necessary to use ``foo\..+`` to do so. value_regex Regex indicating the values to unset for the matching key(s) .. note:: This option behaves differently depending on whether or not ``all`` is set to ``True``. If it is, then all values matching the regex will be deleted (this is the only way to delete multiple values from a multivar). If ``all`` is set to ``False``, then this state will fail if the regex matches more than one value in a multivar. all : False If ``True``, unset all matches repo Location of the git repository for which the config value should be set. Required unless ``global`` is set to ``True``. user User under which to run git commands. By default, commands are 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 global : False If ``True``, this will set a global git config option 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 **Examples:** .. code-block:: yaml # Value matching 'baz' mylocalrepo: git.config_unset: - name: foo.bar - value_regex: 'baz' - repo: /path/to/repo # Ensure entire multivar is unset mylocalrepo: git.config_unset: - name: foo.bar - all: True # Ensure all variables in 'foo' section are unset, including multivars mylocalrepo: git.config_unset: - name: 'foo\..+' - all: True # Ensure that global config value is unset mylocalrepo: git.config_unset: - name: foo.bar - global: True """ ret = { "name": name, "changes": {}, "result": True, "comment": "No matching keys are set", } # Sanitize kwargs and make sure that no invalid ones were passed. This # allows us to accept 'global' as an argument to this function without # shadowing global(), while also not allowing unwanted arguments to be # passed. kwargs = salt.utils.args.clean_kwargs(**kwargs) global_ = kwargs.pop("global", False) all_ = kwargs.pop("all", False) if kwargs: return _fail(ret, salt.utils.args.invalid_kwargs(kwargs, raise_exc=False)) if not global_ and not repo: return _fail( ret, "Non-global config options require the 'repo' argument to be set" ) if not isinstance(name, str): name = str(name) if value_regex is not None: if not isinstance(value_regex, str): value_regex = str(value_regex) # Ensure that the key regex matches the full key name key = "^" + name.lstrip("^").rstrip("$") + "$" # Get matching keys/values pre_matches = __salt__["git.config_get_regexp"]( cwd=repo, key=key, value_regex=value_regex, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, **{"global": global_}, ) if not pre_matches: # No changes need to be made return ret # Perform sanity check on the matches. We can't proceed if the value_regex # matches more than one value in a given key, and 'all' is not set to True if not all_: greedy_matches = [ "{} ({})".format(x, ", ".join(y)) for x, y in pre_matches.items() if len(y) > 1 ] if greedy_matches: if value_regex is not None: return _fail( ret, "Multiple values are matched by value_regex for the " "following keys (set 'all' to True to force removal): " "{}".format("; ".join(greedy_matches)), ) else: return _fail( ret, "Multivar(s) matched by the key expression (set 'all' " "to True to force removal): {}".format("; ".join(greedy_matches)), ) if __opts__["test"]: ret["changes"] = pre_matches return _neutral_test( ret, f"{len(pre_matches)} key(s) would have value(s) unset" ) if value_regex is None: pre = pre_matches else: # Get all keys matching the key expression, so we can accurately report # on changes made. pre = __salt__["git.config_get_regexp"]( cwd=repo, key=key, value_regex=None, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, **{"global": global_}, ) failed = [] # Unset the specified value(s). There is no unset for regexes so loop # through the pre_matches dict and unset each matching key individually. for key_name in pre_matches: try: __salt__["git.config_unset"]( cwd=repo, key=name, value_regex=value_regex, all=all_, user=user, password=password, output_encoding=output_encoding, **{"global": global_}, ) except CommandExecutionError as exc: msg = f"Failed to unset '{key_name}'" if value_regex is not None: msg += " using value_regex '{1}'" msg += ": " + _strip_exc(exc) log.error(msg) failed.append(key_name) if failed: return _fail( ret, "Error(s) occurred unsetting values for the following keys (see " "the minion log for details): {}".format(", ".join(failed)), ) post = __salt__["git.config_get_regexp"]( cwd=repo, key=key, value_regex=None, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, **{"global": global_}, ) for key_name in pre: if key_name not in post: ret["changes"][key_name] = pre[key_name] unset = [x for x in pre[key_name] if x not in post[key_name]] if unset: ret["changes"][key_name] = unset if value_regex is None: post_matches = post else: post_matches = __salt__["git.config_get_regexp"]( cwd=repo, key=key, value_regex=value_regex, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, **{"global": global_}, ) if post_matches: failed = ["{} ({})".format(x, ", ".join(y)) for x, y in post_matches.items()] return _fail(ret, "Failed to unset value(s): {}".format("; ".join(failed))) ret["comment"] = "Value(s) successfully unset" return ret def config_set( name, value=None, multivar=None, repo=None, user=None, password=None, output_encoding=None, **kwargs, ): """ .. versionadded:: 2014.7.0 .. versionchanged:: 2015.8.0 Renamed from ``git.config`` to ``git.config_set``. For earlier versions, use ``git.config``. Ensure that a config value is set to the desired value(s) name Name of the git config value to set value Set a single value for the config item multivar Set multiple values for the config item .. note:: The order matters here, if the same parameters are set but in a different order, they will be removed and replaced in the order specified. .. versionadded:: 2015.8.0 repo Location of the git repository for which the config value should be set. Required unless ``global`` is set to ``True``. user User under which to run git commands. By default, the commands are 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 global : False If ``True``, this will set a global git config option 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 **Local Config Example:** .. code-block:: yaml # Single value mylocalrepo: git.config_set: - name: user.email - value: foo@bar.net - repo: /path/to/repo # Multiple values mylocalrepo: git.config_set: - name: mysection.myattribute - multivar: - foo - bar - baz - repo: /path/to/repo **Global Config Example (User ``foo``):** .. code-block:: yaml mylocalrepo: git.config_set: - name: user.name - value: Foo Bar - user: foo - global: True """ ret = {"name": name, "changes": {}, "result": True, "comment": ""} if value is not None and multivar is not None: return _fail(ret, "Only one of 'value' and 'multivar' is permitted") # Sanitize kwargs and make sure that no invalid ones were passed. This # allows us to accept 'global' as an argument to this function without # shadowing global(), while also not allowing unwanted arguments to be # passed. kwargs = salt.utils.args.clean_kwargs(**kwargs) global_ = kwargs.pop("global", False) if kwargs: return _fail(ret, salt.utils.args.invalid_kwargs(kwargs, raise_exc=False)) if not global_ and not repo: return _fail( ret, "Non-global config options require the 'repo' argument to be set" ) if not isinstance(name, str): name = str(name) if value is not None: if not isinstance(value, str): value = str(value) value_comment = "'" + value + "'" desired = [value] if multivar is not None: if not isinstance(multivar, list): try: multivar = multivar.split(",") except AttributeError: multivar = str(multivar).split(",") else: new_multivar = [] for item in multivar: if isinstance(item, str): new_multivar.append(item) else: new_multivar.append(str(item)) multivar = new_multivar value_comment = multivar desired = multivar # Get current value pre = __salt__["git.config_get"]( cwd=repo, key=name, user=user, password=password, ignore_retcode=True, output_encoding=output_encoding, **{"all": True, "global": global_}, ) if desired == pre: ret["comment"] = "{}'{}' is already set to {}".format( "Global key " if global_ else "", name, value_comment ) return ret if __opts__["test"]: ret["changes"] = {"old": pre, "new": desired} msg = "{}'{}' would be {} {}".format( "Global key " if global_ else "", name, "added as" if pre is None else "set to", value_comment, ) return _neutral_test(ret, msg) try: # Set/update config value post = __salt__["git.config_set"]( cwd=repo, key=name, value=value, multivar=multivar, user=user, password=password, output_encoding=output_encoding, **{"global": global_}, ) except CommandExecutionError as exc: return _fail( ret, "Failed to set {}'{}' to {}: {}".format( "global key " if global_ else "", name, value_comment, _strip_exc(exc) ), ) if pre != post: ret["changes"][name] = {"old": pre, "new": post} if post != desired: return _fail( ret, "Failed to set {}'{}' to {}".format( "global key " if global_ else "", name, value_comment ), ) ret["comment"] = "{}'{}' was {} {}".format( "Global key " if global_ else "", name, "added as" if pre is None else "set to", value_comment, ) return ret