mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Use native "list" subcommand to list git worktrees
This commit is contained in:
parent
911105f27c
commit
5f95851987
1 changed files with 253 additions and 50 deletions
|
@ -35,6 +35,21 @@ def __virtual__():
|
|||
return True if salt.utils.which('git') else False
|
||||
|
||||
|
||||
def _check_worktree_support(failhard=True):
|
||||
'''
|
||||
Ensure that we don't try to operate on worktrees in git < 2.5.0.
|
||||
'''
|
||||
git_version = version(versioninfo=False)
|
||||
if _LooseVersion(git_version) < _LooseVersion('2.5.0'):
|
||||
if failhard:
|
||||
raise CommandExecutionError(
|
||||
'Worktrees are only supported in git 2.5.0 and newer '
|
||||
'(detected git version: ' + git_version + ')'
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _config_getter(get_opt,
|
||||
key,
|
||||
value_regex=None,
|
||||
|
@ -1664,13 +1679,19 @@ def list_worktrees(cwd, stale=False, user=None, **kwargs):
|
|||
'''
|
||||
.. versionadded:: 2015.8.0
|
||||
|
||||
Return a dictionary mapping worktrees to their locations.
|
||||
Returns information on worktrees
|
||||
|
||||
.. versionchanged:: 2015.8.4
|
||||
Version 2.7.0 added the ``list`` subcommand to `git-worktree(1)`_ which
|
||||
provides a lot of additional information. The return data has been
|
||||
changed to include this information, even for pre-2.7.0 versions of
|
||||
git. In addition, if a worktree has a detached head, then any tags
|
||||
which point to the worktree's HEAD will be included in the return data.
|
||||
|
||||
.. note::
|
||||
This information is compiled by analyzing the administrative data in
|
||||
$GIT_DIR/worktrees. By default, only worktrees for which the gitdir is
|
||||
still present are returned, but this can be changed using the ``all``
|
||||
and ``stale`` arguments (described below).
|
||||
By default, only worktrees for which the worktree directory is still
|
||||
present are returned, but this can be changed using the ``all`` and
|
||||
``stale`` arguments (described below).
|
||||
|
||||
cwd
|
||||
The path to the git checkout
|
||||
|
@ -1680,11 +1701,17 @@ def list_worktrees(cwd, stale=False, user=None, **kwargs):
|
|||
by the user under which the minion is running.
|
||||
|
||||
all : False
|
||||
If ``True``, then return all worktrees, including ones whose gitdir is
|
||||
no longer present.
|
||||
If ``True``, then return all worktrees tracked under
|
||||
$GIT_DIR/worktrees, including ones for which the gitdir is no longer
|
||||
present.
|
||||
|
||||
stale : False
|
||||
If ``True``, return only worktrees whose gitdir is no longer present.
|
||||
If ``True``, return *only* worktrees whose gitdir is no longer present.
|
||||
|
||||
.. note::
|
||||
Only one of ``all`` and ``stale`` can be set to ``True``.
|
||||
|
||||
.. _`git-worktree(1)`: http://git-scm.com/docs/git-worktree
|
||||
|
||||
|
||||
CLI Examples:
|
||||
|
@ -1695,6 +1722,8 @@ def list_worktrees(cwd, stale=False, user=None, **kwargs):
|
|||
salt myminion git.list_worktrees /path/to/repo all=True
|
||||
salt myminion git.list_worktrees /path/to/repo stale=True
|
||||
'''
|
||||
if not _check_worktree_support(failhard=True):
|
||||
return {}
|
||||
cwd = _expand_path(cwd, user)
|
||||
kwargs = salt.utils.clean_kwargs(**kwargs)
|
||||
all_ = kwargs.pop('all', False)
|
||||
|
@ -1706,55 +1735,226 @@ def list_worktrees(cwd, stale=False, user=None, **kwargs):
|
|||
'\'all\' and \'stale\' cannot both be set to True'
|
||||
)
|
||||
|
||||
try:
|
||||
worktree_root = rev_parse(cwd, opts=['--git-path', 'worktrees'])
|
||||
except CommandExecutionError as exc:
|
||||
msg = 'Failed to find worktree location for ' + cwd
|
||||
log.error(msg, exc_info_on_loglevel=logging.DEBUG)
|
||||
raise CommandExecutionError(msg)
|
||||
if worktree_root.startswith('.git'):
|
||||
worktree_root = os.path.join(cwd, worktree_root)
|
||||
if not os.path.isdir(worktree_root):
|
||||
return {}
|
||||
def _git_tag_points_at(cwd, rev, user=None):
|
||||
'''
|
||||
Get any tags that point at a
|
||||
'''
|
||||
return _git_run(['git', 'tag', '--points-at', rev],
|
||||
cwd=cwd,
|
||||
runas=user)['stdout'].splitlines()
|
||||
|
||||
worktree_info = {}
|
||||
for worktree_name in os.listdir(worktree_root):
|
||||
gitdir_file = os.path.join(worktree_root, worktree_name, 'gitdir')
|
||||
try:
|
||||
with salt.utils.fopen(gitdir_file, 'r') as fp_:
|
||||
for line in fp_:
|
||||
worktree_loc = line.rstrip('\n')
|
||||
if worktree_loc.endswith('/.git'):
|
||||
worktree_loc = worktree_loc[:-5]
|
||||
worktree_info[worktree_name] = worktree_loc
|
||||
break
|
||||
except (IOError, OSError) as exc:
|
||||
if exc.errno == errno.ENOENT:
|
||||
log.warning(
|
||||
gitdir_file + ' does not exist, data for worktree ' +
|
||||
worktree_name + ' may be corrupted. Try pruning worktrees.'
|
||||
def _desired(is_stale, all_, stale):
|
||||
'''
|
||||
Common logic to determine whether or not to include the worktree info
|
||||
in the return data.
|
||||
'''
|
||||
if is_stale:
|
||||
if not all_ and not stale:
|
||||
# Stale worktrees are not desired, skip this one
|
||||
return False
|
||||
else:
|
||||
if stale:
|
||||
# Only stale worktrees are desired, skip this one
|
||||
return False
|
||||
return True
|
||||
|
||||
def _duplicate_worktree_path(path):
|
||||
'''
|
||||
Log errors to the minion log notifying of duplicate worktree paths.
|
||||
These should not be there, but may show up due to a bug in git 2.7.0.
|
||||
'''
|
||||
log.error(
|
||||
'git.worktree: Duplicate worktree path {0}. This may be caused by '
|
||||
'a known issue in git 2.7.0 (see '
|
||||
'http://permalink.gmane.org/gmane.comp.version-control.git/283998)'
|
||||
.format(path)
|
||||
)
|
||||
|
||||
tracked_data_points = ('worktree', 'HEAD', 'branch')
|
||||
ret = {}
|
||||
git_version = _LooseVersion(version(versioninfo=False))
|
||||
has_native_list_subcommand = git_version >= _LooseVersion('2.7.0')
|
||||
if has_native_list_subcommand:
|
||||
out = _git_run(['git', 'worktree', 'list', '--porcelain'],
|
||||
cwd=cwd,
|
||||
runas=user)
|
||||
if out['retcode'] != 0:
|
||||
msg = 'Failed to list worktrees'
|
||||
if out['stderr']:
|
||||
msg += ': {0}'.format(out['stderr'])
|
||||
raise CommandExecutionError(msg)
|
||||
|
||||
def _untracked_item(line):
|
||||
'''
|
||||
Log a warning
|
||||
'''
|
||||
log.warning(
|
||||
'git.worktree: Untracked line item \'{0}\''.format(line)
|
||||
)
|
||||
|
||||
for individual_worktree in \
|
||||
salt.utils.itertools.split(out['stdout'].strip(), '\n\n'):
|
||||
# Initialize the dict where we're storing the tracked data points
|
||||
worktree_data = dict([(x, '') for x in tracked_data_points])
|
||||
|
||||
for line in salt.utils.itertools.split(individual_worktree, '\n'):
|
||||
try:
|
||||
type_, value = line.strip().split(None, 1)
|
||||
except ValueError:
|
||||
if line == 'detached':
|
||||
type_ = 'branch'
|
||||
value = 'detached'
|
||||
else:
|
||||
_untracked_item(line)
|
||||
continue
|
||||
|
||||
if type_ not in tracked_data_points:
|
||||
_untracked_item(line)
|
||||
continue
|
||||
|
||||
if worktree_data[type_]:
|
||||
log.error(
|
||||
'git.worktree: Unexpected duplicate {0} entry '
|
||||
'\'{1}\', skipping'.format(type_, line)
|
||||
)
|
||||
continue
|
||||
|
||||
worktree_data[type_] = value
|
||||
|
||||
# Check for missing data points
|
||||
missing = [x for x in tracked_data_points if not worktree_data[x]]
|
||||
if missing:
|
||||
log.error(
|
||||
'git.worktree: Incomplete worktree data, missing the '
|
||||
'following information: {0}. Full data below:\n{1}'
|
||||
.format(', '.join(missing), individual_worktree)
|
||||
)
|
||||
continue
|
||||
elif exc.errno == errno.EACCES:
|
||||
raise CommandExecutionError(
|
||||
'Permission denied reading from ' + gitdir_file
|
||||
)
|
||||
|
||||
worktree_is_stale = not os.path.isdir(worktree_data['worktree'])
|
||||
|
||||
if not _desired(worktree_is_stale, all_, stale):
|
||||
continue
|
||||
|
||||
if worktree_data['worktree'] in ret:
|
||||
_duplicate_worktree_path(worktree_data['worktree'])
|
||||
|
||||
wt_ptr = ret.setdefault(worktree_data['worktree'], {})
|
||||
wt_ptr['stale'] = worktree_is_stale
|
||||
wt_ptr['HEAD'] = worktree_data['HEAD']
|
||||
wt_ptr['detached'] = worktree_data['branch'] == 'detached'
|
||||
if wt_ptr['detached']:
|
||||
wt_ptr['branch'] = None
|
||||
# Check to see if HEAD points at a tag
|
||||
tags_found = _git_tag_points_at(cwd, wt_ptr['HEAD'], user)
|
||||
if tags_found:
|
||||
wt_ptr['tags'] = tags_found
|
||||
else:
|
||||
raise CommandExecutionError(
|
||||
'Error {0} encountered reading from {1}: {2}'.format(
|
||||
exc.errno, gitdir_file, exc.strerror
|
||||
wt_ptr['branch'] = \
|
||||
worktree_data['branch'].replace('refs/heads/', '', 1)
|
||||
|
||||
return ret
|
||||
|
||||
else:
|
||||
toplevel = _get_toplevel(cwd, user)
|
||||
try:
|
||||
worktree_root = rev_parse(cwd,
|
||||
opts=['--git-path', 'worktrees'],
|
||||
user=user)
|
||||
except CommandExecutionError as exc:
|
||||
msg = 'Failed to find worktree location for ' + cwd
|
||||
log.error(msg, exc_info_on_loglevel=logging.DEBUG)
|
||||
raise CommandExecutionError(msg)
|
||||
if worktree_root.startswith('.git'):
|
||||
worktree_root = os.path.join(cwd, worktree_root)
|
||||
if not os.path.isdir(worktree_root):
|
||||
raise CommandExecutionError(
|
||||
'Worktree admin directory {0} not present'
|
||||
.format(worktree_root)
|
||||
)
|
||||
|
||||
def _read_file(path):
|
||||
'''
|
||||
Return contents of a single line file with EOF newline stripped
|
||||
'''
|
||||
try:
|
||||
with salt.utils.fopen(path, 'r') as fp_:
|
||||
for line in fp_:
|
||||
ret = line.strip()
|
||||
# Ignore other lines, if they exist (which they
|
||||
# shouldn't)
|
||||
break
|
||||
return ret
|
||||
except (IOError, OSError) as exc:
|
||||
if exc.errno == errno.ENOENT:
|
||||
raise CommandExecutionError(
|
||||
'{0} does not exist'.format(path)
|
||||
)
|
||||
elif exc.errno == errno.EACCES:
|
||||
raise CommandExecutionError(
|
||||
'Permission denied reading from {0}'.format(path)
|
||||
)
|
||||
else:
|
||||
raise CommandExecutionError(
|
||||
'Error {0} encountered reading from {1}: {2}'.format(
|
||||
exc.errno, path, exc.strerror
|
||||
)
|
||||
)
|
||||
|
||||
for worktree_name in os.listdir(worktree_root):
|
||||
admin_dir = os.path.join(worktree_root, worktree_name)
|
||||
gitdir_file = os.path.join(admin_dir, 'gitdir')
|
||||
head_file = os.path.join(admin_dir, 'HEAD')
|
||||
|
||||
wt_loc = _read_file(gitdir_file)
|
||||
head_ref = _read_file(head_file)
|
||||
|
||||
if not os.path.isabs(wt_loc):
|
||||
log.error(
|
||||
'Non-absolute path found in {0}. If git 2.7.0 was '
|
||||
'installed and then downgraded, this was likely caused '
|
||||
'by a known issue in git 2.7.0. See '
|
||||
'http://permalink.gmane.org/gmane.comp.version-control'
|
||||
'.git/283998 for more information.'.format(gitdir_file)
|
||||
)
|
||||
# Emulate what 'git worktree list' does under-the-hood, and
|
||||
# that is using the toplevel directory. It will still give
|
||||
# inaccurate results, but will avoid a traceback.
|
||||
wt_loc = toplevel
|
||||
|
||||
if all_ or not worktree_info:
|
||||
return worktree_info
|
||||
if wt_loc.endswith('/.git'):
|
||||
wt_loc = wt_loc[:-5]
|
||||
|
||||
worktree_is_stale = not os.path.isdir(wt_loc)
|
||||
|
||||
if not _desired(worktree_is_stale, all_, stale):
|
||||
continue
|
||||
|
||||
if wt_loc in ret:
|
||||
_duplicate_worktree_path(wt_loc)
|
||||
|
||||
if head_ref.startswith('ref: '):
|
||||
head_ref = head_ref.split(None, 1)[-1]
|
||||
wt_branch = head_ref.replace('refs/heads/', '', 1)
|
||||
wt_head = rev_parse(cwd, rev=head_ref, user=user)
|
||||
wt_detached = False
|
||||
else:
|
||||
wt_branch = None
|
||||
wt_head = head_ref
|
||||
wt_detached = True
|
||||
|
||||
wt_ptr = ret.setdefault(wt_loc, {})
|
||||
wt_ptr['stale'] = worktree_is_stale
|
||||
wt_ptr['branch'] = wt_branch
|
||||
wt_ptr['HEAD'] = wt_head
|
||||
wt_ptr['detached'] = wt_detached
|
||||
|
||||
# Check to see if HEAD points at a tag
|
||||
if wt_detached:
|
||||
tags_found = _git_tag_points_at(cwd, wt_head, user)
|
||||
if tags_found:
|
||||
wt_ptr['tags'] = tags_found
|
||||
|
||||
ret = {}
|
||||
for worktree_name, worktree_loc in six.iteritems(worktree_info):
|
||||
worktree_is_stale = not os.path.isdir(worktree_loc)
|
||||
if (stale and worktree_is_stale) \
|
||||
or (not stale and not worktree_is_stale):
|
||||
ret[worktree_name] = worktree_loc
|
||||
return ret
|
||||
|
||||
|
||||
|
@ -2098,7 +2298,7 @@ def merge_base(cwd,
|
|||
# returns an identical commit to the resolved first ref, we know
|
||||
# that the first ref is an ancestor of the second ref.
|
||||
first_commit = rev_parse(cwd,
|
||||
refs[0],
|
||||
rev=refs[0],
|
||||
opts=['--verify'],
|
||||
user=user,
|
||||
ignore_retcode=ignore_retcode)
|
||||
|
@ -3329,6 +3529,7 @@ def worktree_add(cwd,
|
|||
salt myminion git.worktree_add /path/to/repo/main ../hotfix ref=origin/master
|
||||
salt myminion git.worktree_add /path/to/repo/main ../hotfix branch=hotfix21 ref=v2.1.9.3
|
||||
'''
|
||||
_check_worktree_support()
|
||||
kwargs = salt.utils.clean_kwargs(**kwargs)
|
||||
branch_ = kwargs.pop('branch', None)
|
||||
if kwargs:
|
||||
|
@ -3434,6 +3635,7 @@ def worktree_prune(cwd,
|
|||
salt myminion git.worktree_prune /path/to/repo dry_run=True
|
||||
salt myminion git.worktree_prune /path/to/repo expire=1.day.ago
|
||||
'''
|
||||
_check_worktree_support()
|
||||
cwd = _expand_path(cwd, user)
|
||||
command = ['git', 'worktree', 'prune']
|
||||
if dry_run:
|
||||
|
@ -3480,6 +3682,7 @@ def worktree_rm(cwd, user=None):
|
|||
|
||||
salt myminion git.worktree_rm /path/to/worktree
|
||||
'''
|
||||
_check_worktree_support()
|
||||
cwd = _expand_path(cwd, user)
|
||||
if not os.path.exists(cwd):
|
||||
raise CommandExecutionError(cwd + ' does not exist')
|
||||
|
|
Loading…
Add table
Reference in a new issue