mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 17:09:03 +00:00
add git.detached state
This commit is contained in:
parent
ac183e3c4e
commit
ca0c8ef5f4
@ -1588,6 +1588,476 @@ def present(name,
|
||||
return ret
|
||||
|
||||
|
||||
def detached(name,
|
||||
ref,
|
||||
target=None,
|
||||
remote='origin',
|
||||
user=None,
|
||||
force_clone=False,
|
||||
force_checkout=False,
|
||||
fetch_remote=True,
|
||||
hard_reset=False,
|
||||
submodules=False,
|
||||
identity=None,
|
||||
https_user=None,
|
||||
https_pass=None,
|
||||
onlyif=False,
|
||||
unless=False,
|
||||
**kwargs):
|
||||
'''
|
||||
.. versionadded:: Boron
|
||||
|
||||
Make sure a repository is cloned to the given target directory and is
|
||||
a detached HEAD checkout of the commit ID resolved from ``ref``.
|
||||
|
||||
name
|
||||
Address of the remote repository.
|
||||
|
||||
ref
|
||||
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.
|
||||
|
||||
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 server to a private key to use over SSH
|
||||
Key can be specified as a SaltStack file server URL
|
||||
eg. salt://location/identity_file
|
||||
|
||||
https_user
|
||||
HTTP Basic Auth username for HTTPS (only) clones
|
||||
|
||||
https_pass
|
||||
HTTP Basic Auth password for HTTPS (only) clones
|
||||
|
||||
onlyif
|
||||
A command to run as a check, run the named command only if the command
|
||||
passed to the ``onlyif`` option returns true
|
||||
|
||||
unless
|
||||
A command to run as a check, only run the named command if the command
|
||||
passed to the ``unless`` option returns false
|
||||
|
||||
'''
|
||||
|
||||
ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
|
||||
|
||||
kwargs = salt.utils.clean_kwargs(**kwargs)
|
||||
if kwargs:
|
||||
return _fail(
|
||||
ret,
|
||||
salt.utils.invalid_kwargs(kwargs, raise_exc=False)
|
||||
)
|
||||
|
||||
if not ref:
|
||||
return _fail(
|
||||
ret,
|
||||
'\'{0}\' is not a valid value for the \'ref\' argument'.format(ref)
|
||||
)
|
||||
|
||||
if not target:
|
||||
return _fail(
|
||||
ret,
|
||||
'\'{0}\' is not a valid value for the \'target\' argument'.format(ref)
|
||||
)
|
||||
|
||||
# Ensure that certain arguments are strings to ensure that comparisons work
|
||||
if not isinstance(ref, six.string_types):
|
||||
ref = str(ref)
|
||||
if target is not None:
|
||||
if not isinstance(target, six.string_types):
|
||||
target = str(target)
|
||||
if not os.path.isabs(target):
|
||||
return _fail(
|
||||
ret,
|
||||
'Target \'{0}\' is not an absolute path'.format(target)
|
||||
)
|
||||
if user is not None and not isinstance(user, six.string_types):
|
||||
user = str(user)
|
||||
if remote is not None and not isinstance(remote, six.string_types):
|
||||
remote = str(remote)
|
||||
if identity is not None:
|
||||
if isinstance(identity, six.string_types):
|
||||
identity = [identity]
|
||||
elif not isinstance(identity, list):
|
||||
return _fail(ret, 'Identity must be either a list or a string')
|
||||
for ident_path in identity:
|
||||
if 'salt://' in ident_path:
|
||||
try:
|
||||
ident_path = __salt__['cp.cache_file'](ident_path)
|
||||
except IOError as exc:
|
||||
log.error(
|
||||
'Failed to cache {0}: {1}'.format(ident_path, exc)
|
||||
)
|
||||
return _fail(
|
||||
ret,
|
||||
'Identity \'{0}\' does not exist.'.format(
|
||||
ident_path
|
||||
)
|
||||
)
|
||||
if not os.path.isabs(ident_path):
|
||||
return _fail(
|
||||
ret,
|
||||
'Identity \'{0}\' is not an absolute path'.format(
|
||||
ident_path
|
||||
)
|
||||
)
|
||||
if https_user is not None and not isinstance(https_user, six.string_types):
|
||||
https_user = str(https_user)
|
||||
if https_pass is not None and not isinstance(https_pass, six.string_types):
|
||||
https_pass = str(https_pass)
|
||||
|
||||
if os.path.isfile(target):
|
||||
return _fail(
|
||||
ret,
|
||||
'Target \'{0}\' exists and is a regular file, cannot proceed'
|
||||
.format(target)
|
||||
)
|
||||
|
||||
try:
|
||||
desired_fetch_url = salt.utils.url.add_http_basic_auth(
|
||||
name,
|
||||
https_user,
|
||||
https_pass,
|
||||
https_only=True
|
||||
)
|
||||
except ValueError as exc:
|
||||
return _fail(ret, exc.__str__())
|
||||
|
||||
redacted_fetch_url = salt.utils.url.redact_http_basic_auth(desired_fetch_url)
|
||||
|
||||
# Check if onlyif or unless conditions match
|
||||
run_check_cmd_kwargs = {'runas': user}
|
||||
if 'shell' in __grains__:
|
||||
run_check_cmd_kwargs['shell'] = __grains__['shell']
|
||||
cret = mod_run_check(
|
||||
run_check_cmd_kwargs, onlyif, unless
|
||||
)
|
||||
if isinstance(cret, dict):
|
||||
ret.update(cret)
|
||||
return ret
|
||||
|
||||
# Determine if supplied ref is a hash
|
||||
remote_ref_type = 'ref'
|
||||
if len(ref) <= 40 \
|
||||
and all(x in string.hexdigits for x in ref):
|
||||
ref = ref.lower()
|
||||
remote_ref_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):
|
||||
# Target directory is a git repository or git worktree
|
||||
|
||||
local_commit_id = _get_local_rev_and_branch(target, user)[0]
|
||||
|
||||
if remote_ref_type is 'hash' and __salt__['git.describe'](ref):
|
||||
# The ref 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,
|
||||
redact_auth=False)
|
||||
|
||||
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,
|
||||
'Remote {0} would be set to {1}'.format(
|
||||
remote, name
|
||||
)
|
||||
)
|
||||
|
||||
__salt__['git.remote_set'](target,
|
||||
url=name,
|
||||
remote=remote,
|
||||
user=user,
|
||||
https_user=https_user,
|
||||
https_pass=https_pass)
|
||||
comments.append(
|
||||
'Remote {0} updated from \'{1}\' to \'{2}\''.format(
|
||||
remote,
|
||||
str(current_fetch_url),
|
||||
name
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
# Clone repository
|
||||
if os.path.isdir(target):
|
||||
if force_clone:
|
||||
# Clone is required, and target directory exists, but the
|
||||
# ``force`` option is enabled, so we need to clear out its
|
||||
# contents to proceed.
|
||||
if __opts__['test']:
|
||||
return _neutral_test(
|
||||
ret,
|
||||
'Target directory {0} exists. Since force_clone=True, '
|
||||
'the contents of {0} would be deleted, and {1} would '
|
||||
'be cloned into this directory.'.format(target, name)
|
||||
)
|
||||
log.debug(
|
||||
'Removing contents of {0} to clone repository {1} in its '
|
||||
'place (force_clone=True set in git.detached state)'
|
||||
.format(target, name)
|
||||
)
|
||||
try:
|
||||
if os.path.islink(target):
|
||||
os.unlink(target)
|
||||
else:
|
||||
salt.utils.rm_rf(target)
|
||||
except OSError as exc:
|
||||
return _fail(
|
||||
ret,
|
||||
'Unable to remove {0}: {1}'.format(target, exc),
|
||||
comments
|
||||
)
|
||||
else:
|
||||
ret['changes']['forced clone'] = True
|
||||
elif os.listdir(target):
|
||||
# Clone is required, but target dir exists and is non-empty. We
|
||||
# can't proceed.
|
||||
return _fail(
|
||||
ret,
|
||||
'Target \'{0}\' exists, is non-empty and is not a git '
|
||||
'repository. Set the \'force_clone\' option to True to '
|
||||
'remove this directory\'s contents and proceed with '
|
||||
'cloning the remote repository'.format(target)
|
||||
)
|
||||
|
||||
log.debug(
|
||||
'Target {0} is not found, \'git clone\' is required'.format(target)
|
||||
)
|
||||
if __opts__['test']:
|
||||
return _neutral_test(
|
||||
ret,
|
||||
'Repository {0} would be cloned to {1}'.format(
|
||||
name, target
|
||||
)
|
||||
)
|
||||
try:
|
||||
clone_opts = ['--no-checkout']
|
||||
if remote != 'origin':
|
||||
clone_opts.extend(['--origin', remote])
|
||||
|
||||
__salt__['git.clone'](target,
|
||||
name,
|
||||
user=user,
|
||||
opts=clone_opts,
|
||||
identity=identity,
|
||||
https_user=https_user,
|
||||
https_pass=https_pass)
|
||||
comments.append(
|
||||
'{0} cloned to {1}'.format(
|
||||
name,
|
||||
target
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
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 = [
|
||||
'refs/heads/*:refs/remotes/{0}/*'.format(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,
|
||||
'Repository remote {0} would be fetched'.format(
|
||||
remote
|
||||
)
|
||||
)
|
||||
try:
|
||||
fetch_changes = __salt__['git.fetch'](
|
||||
target,
|
||||
remote=remote,
|
||||
force=True,
|
||||
refspecs=refspecs,
|
||||
user=user,
|
||||
identity=identity)
|
||||
except CommandExecutionError as exc:
|
||||
msg = 'Fetch failed'
|
||||
msg += ':\n\n' + str(exc)
|
||||
return _fail(ret, msg, comments)
|
||||
else:
|
||||
if fetch_changes:
|
||||
comments.append(
|
||||
'Remote {0} was fetched, resulting in updated '
|
||||
'refs'.format(remote)
|
||||
)
|
||||
|
||||
#get refs and checkout
|
||||
checkout_commit_id = ''
|
||||
if remote_ref_type is 'hash':
|
||||
if __salt__['git.describe'](ref):
|
||||
checkout_commit_id = ref
|
||||
else:
|
||||
return _fail(
|
||||
ret,
|
||||
'Ref does not exist: {0}'.format(ref)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
all_remote_refs = __salt__['git.remote_refs'](
|
||||
target,
|
||||
user=user,
|
||||
identity=identity,
|
||||
https_user=https_user,
|
||||
https_pass=https_pass,
|
||||
ignore_retcode=False)
|
||||
|
||||
if 'refs/remotes/'+remote+'/'+ref in all_remote_refs:
|
||||
checkout_commit_id = all_remote_refs['refs/remotes/'+remote+'/'+ref]
|
||||
elif 'refs/tags/'+ref in all_remote_refs:
|
||||
checkout_commit_id = all_remote_refs['refs/tags/'+ref]
|
||||
else:
|
||||
return _fail(
|
||||
ret,
|
||||
'Ref {0} does not exist'.format(ref)
|
||||
)
|
||||
|
||||
except CommandExecutionError as exc:
|
||||
return _fail(
|
||||
ret,
|
||||
'Failed to list refs for {0}: {1}'.format(remote, _strip_exc(exc))
|
||||
)
|
||||
|
||||
if hard_reset:
|
||||
if __opts__['test']:
|
||||
return _neutral_test(
|
||||
ret,
|
||||
'Hard reset to HEAD would be performed on {0}'.format(
|
||||
target
|
||||
)
|
||||
)
|
||||
__salt__['git.reset'](
|
||||
target,
|
||||
opts=['--hard', 'HEAD'],
|
||||
user=user
|
||||
)
|
||||
comments.append(
|
||||
'Repository was reset to HEAD before checking out ref'
|
||||
)
|
||||
|
||||
# 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 {0} would be checked out at {1}'.format(
|
||||
checkout_commit_id,
|
||||
target
|
||||
)
|
||||
)
|
||||
__salt__['git.checkout'](target,
|
||||
checkout_commit_id,
|
||||
force=force_checkout,
|
||||
user=user)
|
||||
comments.append(
|
||||
'Commit ID {0} was checked out at {1}'.format(
|
||||
checkout_commit_id,
|
||||
target
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
new_rev = __salt__['git.revision'](
|
||||
cwd=target,
|
||||
user=user,
|
||||
ignore_retcode=True)
|
||||
except CommandExecutionError:
|
||||
new_rev = None
|
||||
|
||||
if submodules:
|
||||
__salt__['git.submodule'](target,
|
||||
'update',
|
||||
opts=['--init', '--recursive'],
|
||||
user=user,
|
||||
identity=identity)
|
||||
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 config_unset(name,
|
||||
value_regex=None,
|
||||
repo=None,
|
||||
|
Loading…
Reference in New Issue
Block a user