Improve support for default branches (#231, #232)

Unless a branch is specified, the default remote HEAD is used during
clone. Also a local master branch is not created if it is not the remote
HEAD.
This commit is contained in:
Tim Byrne 2020-11-28 11:23:46 -06:00
parent 4cb13d5d08
commit 4b5f16d73a
No known key found for this signature in database
GPG Key ID: 14DB4FC2465A4B12
4 changed files with 90 additions and 21 deletions

View File

@ -70,6 +70,9 @@ def test_clone(
# clone should succeed, and repo should be configured properly # clone should succeed, and repo should be configured properly
assert successful_clone(run, paths, repo_config) assert successful_clone(run, paths, repo_config)
# these clones should have master as HEAD
verify_head(paths, 'master')
# ensure conflicts are handled properly # ensure conflicts are handled properly
if conflicts: if conflicts:
assert 'NOTE' in run.out assert 'NOTE' in run.out
@ -162,6 +165,7 @@ def test_clone_bootstrap(
assert BOOTSTRAP_MSG not in run.out assert BOOTSTRAP_MSG not in run.out
assert successful_clone(run, paths, repo_config, expected_code) assert successful_clone(run, paths, repo_config, expected_code)
verify_head(paths, 'master')
if not bs_exists: if not bs_exists:
assert BOOTSTRAP_MSG not in run.out assert BOOTSTRAP_MSG not in run.out
@ -230,6 +234,7 @@ def test_clone_perms(
) )
assert successful_clone(run, paths, repo_config) assert successful_clone(run, paths, repo_config)
verify_head(paths, 'master')
if in_work: if in_work:
# private directories which already exist, should be left as they are, # private directories which already exist, should be left as they are,
# which in this test is "insecure". # which in this test is "insecure".
@ -260,7 +265,8 @@ def test_clone_perms(
@pytest.mark.usefixtures('remote') @pytest.mark.usefixtures('remote')
@pytest.mark.parametrize('branch', ['master', 'valid', 'invalid']) @pytest.mark.parametrize(
'branch', ['master', 'default', 'valid', 'invalid'])
def test_alternate_branch(runner, paths, yadm_cmd, repo_config, branch): def test_alternate_branch(runner, paths, yadm_cmd, repo_config, branch):
"""Test cloning a branch other than master""" """Test cloning a branch other than master"""
@ -269,6 +275,12 @@ def test_alternate_branch(runner, paths, yadm_cmd, repo_config, branch):
os.system( os.system(
f'GIT_DIR="{paths.remote}" git commit ' f'GIT_DIR="{paths.remote}" git commit '
f'--allow-empty -m "This branch is valid"') f'--allow-empty -m "This branch is valid"')
if branch != 'default':
# When branch == 'default', the "default" branch of the remote repo
# will remain "valid" to validate identification the correct default
# branch by inspecting the repo. Otherwise it will be set back to
# "master"
os.system(f'GIT_DIR="{paths.remote}" git checkout master')
# clear out the work path # clear out the work path
paths.work.remove() paths.work.remove()
@ -278,7 +290,7 @@ def test_alternate_branch(runner, paths, yadm_cmd, repo_config, branch):
# run the clone command # run the clone command
args = ['clone', '-w', paths.work] args = ['clone', '-w', paths.work]
if branch != 'master': if branch not in ['master', 'default']:
args += ['-b', branch] args += ['-b', branch]
args += [remote_url] args += [remote_url]
run = runner(command=yadm_cmd(*args)) run = runner(command=yadm_cmd(*args))
@ -298,10 +310,12 @@ def test_alternate_branch(runner, paths, yadm_cmd, repo_config, branch):
assert run.err == '' assert run.err == ''
assert f'origin\t{remote_url}' in run.out assert f'origin\t{remote_url}' in run.out
run = runner(command=yadm_cmd('show')) run = runner(command=yadm_cmd('show'))
if branch == 'valid': if branch == 'master':
assert 'This branch is valid' in run.out
else:
assert 'Initial commit' in run.out assert 'Initial commit' in run.out
verify_head(paths, 'master')
else:
assert 'This branch is valid' in run.out
verify_head(paths, 'valid')
def successful_clone(run, paths, repo_config, expected_code=0): def successful_clone(run, paths, repo_config, expected_code=0):
@ -324,3 +338,16 @@ def remote(paths, ds1_repo_copy):
# cannot be applied to another fixture. # cannot be applied to another fixture.
paths.remote.remove() paths.remote.remove()
paths.repo.move(paths.remote) paths.repo.move(paths.remote)
def test_no_repo(runner, yadm_cmd, ):
"""Test cloning without specifying a repo"""
run = runner(command=yadm_cmd('clone'))
assert run.failure
assert run.err == ''
assert 'ERROR: No repository provided' in run.out
def verify_head(paths, branch):
"""Assert the local repo has the correct head branch"""
assert paths.repo.join('HEAD').read() == f'ref: refs/heads/{branch}\n'

View File

@ -0,0 +1,27 @@
"""Unit tests: _default_remote_branch()"""
import pytest
@pytest.mark.parametrize('condition', ['found', 'missing'])
def test(runner, paths, condition):
"""Test _default_remote_branch()"""
test_branch = 'test/branch'
output = f'ref: refs/heads/{test_branch}\\tHEAD\\n'
if condition == 'missing':
output = 'output that is missing ref'
script = f"""
YADM_TEST=1 source {paths.pgm}
function git() {{
printf '{output}';
printf 'mock stderr\\n' 1>&2
}}
_default_remote_branch URL
"""
print(condition)
run = runner(command=['bash'], inp=script)
assert run.success
assert run.err == ''
if condition == 'found':
assert run.out.strip() == test_branch
else:
assert run.out.strip() == 'master'

38
yadm
View File

@ -743,13 +743,23 @@ function clean() {
} }
function _default_remote_branch() {
local ls_remote
ls_remote=$("$GIT_PROGRAM" ls-remote -q --symref "$1" 2>/dev/null)
match="^ref:[[:blank:]]+refs/heads/([^[:blank:]]+)"
if [[ "$ls_remote" =~ $match ]] ; then
echo "${BASH_REMATCH[1]}"
else
echo master
fi
}
function clone() { function clone() {
DO_BOOTSTRAP=1 DO_BOOTSTRAP=1
local branch local branch=
branch="master"
clone_args=() local repo_url=
while [[ $# -gt 0 ]] ; do while [[ $# -gt 0 ]] ; do
key="$1" key="$1"
case $key in case $key in
@ -766,22 +776,29 @@ function clone() {
--no-bootstrap) # prevent bootstrap, without prompt --no-bootstrap) # prevent bootstrap, without prompt
DO_BOOTSTRAP=3 DO_BOOTSTRAP=3
;; ;;
*) # main arguments are kept intact *) # use first found argument as the URL
clone_args+=("$1") [ -z "$repo_url" ] && repo_url="$1"
;; ;;
esac esac
shift shift
done done
[ -z "$repo_url" ] && error_out "No repository provided"
[ -z "$branch" ] && branch=$(_default_remote_branch "$repo_url")
[ -n "$DEBUG" ] && display_private_perms "initial" [ -n "$DEBUG" ] && display_private_perms "initial"
# shellcheck disable=SC2119
# clone will begin with a bare repo # clone will begin with a bare repo
local empty= init
init $empty
# configure local HEAD with the correct branch
printf 'ref: refs/heads/%s\n' "$branch" > "${YADM_REPO}/HEAD"
# add the specified remote, and configure the repo to track origin/$branch # add the specified remote, and configure the repo to track origin/$branch
debug "Adding remote to new repo" debug "Adding remote to new repo"
"$GIT_PROGRAM" remote add origin "${clone_args[@]}" "$GIT_PROGRAM" remote add origin "$repo_url"
debug "Configuring new repo to track origin/${branch}" debug "Configuring new repo to track origin/${branch}"
"$GIT_PROGRAM" config "branch.${branch}.remote" origin "$GIT_PROGRAM" config "branch.${branch}.remote" origin
"$GIT_PROGRAM" config "branch.${branch}.merge" "refs/heads/${branch}" "$GIT_PROGRAM" config "branch.${branch}.merge" "refs/heads/${branch}"
@ -791,13 +808,13 @@ function clone() {
"$GIT_PROGRAM" fetch origin || { "$GIT_PROGRAM" fetch origin || {
debug "Removing repo after failed clone" debug "Removing repo after failed clone"
rm -rf "$YADM_REPO" rm -rf "$YADM_REPO"
error_out "Unable to fetch origin ${clone_args[0]}" error_out "Unable to fetch origin $repo_url"
} }
debug "Verifying '${branch}' is a valid branch to merge" debug "Verifying '${branch}' is a valid branch to merge"
[ -f "${YADM_REPO}/refs/remotes/origin/${branch}" ] || { [ -f "${YADM_REPO}/refs/remotes/origin/${branch}" ] || {
debug "Removing repo after failed clone" debug "Removing repo after failed clone"
rm -rf "$YADM_REPO" rm -rf "$YADM_REPO"
error_out "Clone failed, 'origin/${branch}' does not exist in ${clone_args[0]}" error_out "Clone failed, 'origin/${branch}' does not exist in $repo_url"
} }
if [ "$YADM_WORK" = "$HOME" ]; then if [ "$YADM_WORK" = "$HOME" ]; then
@ -1164,6 +1181,7 @@ EOF
} }
# shellcheck disable=SC2120
function init() { function init() {
# safety check, don't attempt to init when the repo is already present # safety check, don't attempt to init when the repo is already present

9
yadm.1
View File

@ -116,12 +116,10 @@ if it exists.
.BI clone " url .BI clone " url
Clone a remote repository for tracking dotfiles. Clone a remote repository for tracking dotfiles.
After the contents of the remote repository have been fetched, a "merge" of After the contents of the remote repository have been fetched, a "merge" of
.I origin/master the remote HEAD branch is attempted.
is attempted.
If there are conflicting files already present in the If there are conflicting files already present in the
.IR work-tree , .IR work-tree ,
this merge will fail and instead a "reset" of this merge will fail and instead a "reset" of the remote HEAD branch
.I origin/master
will be done, followed by a "stash". This "stash" operation will preserve the will be done, followed by a "stash". This "stash" operation will preserve the
original data. original data.
@ -154,8 +152,7 @@ but this can be overridden with the
.BR -w " option. .BR -w " option.
yadm can be forced to overwrite an existing repository by providing the yadm can be forced to overwrite an existing repository by providing the
.BR -f " option. .BR -f " option.
If you want to use a branch other than If you want to use a branch other than the remote HEAD branch
.IR origin/master ,
you can specify it using the you can specify it using the
.BR -b " option. .BR -b " option.
By default yadm will ask the user if the bootstrap program should be run (if it By default yadm will ask the user if the bootstrap program should be run (if it