Mini Shell
"""
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
Zerion Mini Shell 1.0