Support `-b <branch>` when cloning (#133)

This commit is contained in:
Tim Byrne 2019-10-07 08:36:32 -05:00
parent 6a3199ceea
commit f3bde37f78
No known key found for this signature in database
GPG Key ID: 14DB4FC2465A4B12
5 changed files with 134 additions and 17 deletions

View File

@ -55,7 +55,7 @@ if declare -F _git > /dev/null; then
case "$antepenultimate" in case "$antepenultimate" in
clone) clone)
COMPREPLY=( $(compgen -W "-f -w --bootstrap --no-bootstrap" -- "$current") ) COMPREPLY=( $(compgen -W "-f -w -b --bootstrap --no-bootstrap" -- "$current") )
return 0 return 0
;; ;;
esac esac

View File

@ -251,6 +251,51 @@ def test_clone_perms(
f'.{private_type} has not been secured by auto.perms') f'.{private_type} has not been secured by auto.perms')
@pytest.mark.usefixtures('remote')
@pytest.mark.parametrize('branch', ['master', 'valid', 'invalid'])
def test_alternate_branch(runner, paths, yadm_y, repo_config, branch):
"""Test cloning a branch other than master"""
# add a "valid" branch to the remote
os.system(f'GIT_DIR="{paths.remote}" git checkout -b valid')
os.system(
f'GIT_DIR="{paths.remote}" git commit '
f'--allow-empty -m "This branch is valid"')
# clear out the work path
paths.work.remove()
paths.work.mkdir()
remote_url = f'file://{paths.remote}'
# run the clone command
args = ['clone', '-w', paths.work]
if branch != 'master':
args += ['-b', branch]
args += [remote_url]
run = runner(command=yadm_y(*args))
if branch == 'invalid':
assert run.failure
assert 'ERROR: Clone failed' in run.out
assert f"'origin/{branch}' does not exist in {remote_url}" in run.out
else:
assert successful_clone(run, paths, repo_config)
# confirm correct Git origin
run = runner(
command=('git', 'remote', '-v', 'show'),
env={'GIT_DIR': paths.repo})
assert run.success
assert run.err == ''
assert f'origin\t{remote_url}' in run.out
run = runner(command=yadm_y('show'))
if branch == 'valid':
assert 'This branch is valid' in run.out
else:
assert 'Initial commit' in run.out
def successful_clone(run, paths, repo_config, expected_code=0): def successful_clone(run, paths, repo_config, expected_code=0):
"""Assert clone is successful""" """Assert clone is successful"""
assert run.code == expected_code assert run.code == expected_code

View File

@ -0,0 +1,40 @@
"""Unit tests: is_valid_branch_name"""
import pytest
# Git branches do not allow:
# * path component that begins with "."
# * double dot
# * "~", "^", ":", "\", space
# * end with a "/"
# * end with ".lock"
@pytest.mark.parametrize(
'branch, expected', [
('master', 'valid'),
('path/branch', 'valid'),
('path/.branch', 'invalid'),
('path..branch', 'invalid'),
('path~branch', 'invalid'),
('path^branch', 'invalid'),
('path:branch', 'invalid'),
('path\\branch', 'invalid'),
('path branch', 'invalid'),
('path/branch/', 'invalid'),
('branch.lock', 'invalid'),
])
def test_is_valid_branch_name(runner, yadm, branch, expected):
"""Test function is_valid_branch_name()"""
script = f"""
YADM_TEST=1 source {yadm}
if is_valid_branch_name "{branch}"; then
echo valid
else
echo invalid
fi
"""
run = runner(command=['bash'], inp=script)
assert run.success
assert run.err == ''
assert run.out.strip() == expected

54
yadm
View File

@ -610,11 +610,20 @@ function clean() {
function clone() { function clone() {
DO_BOOTSTRAP=1 DO_BOOTSTRAP=1
local branch
branch="master"
clone_args=() clone_args=()
while [[ $# -gt 0 ]] ; do while [[ $# -gt 0 ]] ; do
key="$1" key="$1"
case $key in case $key in
-b)
if ! is_valid_branch_name "$2"; then
error_out "You must provide a branch name when using '-b'"
fi
branch="$2"
shift
;;
--bootstrap) # force bootstrap, without prompt --bootstrap) # force bootstrap, without prompt
DO_BOOTSTRAP=2 DO_BOOTSTRAP=2
;; ;;
@ -634,12 +643,12 @@ function clone() {
local empty= local empty=
init $empty init $empty
# add the specified remote, and configure the repo to track origin/master # 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 "${clone_args[@]}"
debug "Configuring new repo to track origin/master" debug "Configuring new repo to track origin/${branch}"
"$GIT_PROGRAM" config branch.master.remote origin "$GIT_PROGRAM" config "branch.${branch}.remote" origin
"$GIT_PROGRAM" config branch.master.merge refs/heads/master "$GIT_PROGRAM" config "branch.${branch}.merge" "refs/heads/${branch}"
# fetch / merge (and possibly fallback to reset) # fetch / merge (and possibly fallback to reset)
debug "Doing an initial fetch of the origin" debug "Doing an initial fetch of the origin"
@ -648,30 +657,36 @@ function clone() {
rm -rf "$YADM_REPO" rm -rf "$YADM_REPO"
error_out "Unable to fetch origin ${clone_args[0]}" error_out "Unable to fetch origin ${clone_args[0]}"
} }
debug "Verifying '${branch}' is a valid branch to merge"
[ -f "${YADM_REPO}/refs/remotes/origin/${branch}" ] || {
debug "Removing repo after failed clone"
rm -rf "$YADM_REPO"
error_out "Clone failed, 'origin/${branch}' does not exist in ${clone_args[0]}"
}
debug "Determining if repo tracks private directories" debug "Determining if repo tracks private directories"
for private_dir in .ssh/ .gnupg/; do for private_dir in .ssh/ .gnupg/; do
found_log=$("$GIT_PROGRAM" log -n 1 origin/master -- "$private_dir" 2>/dev/null) found_log=$("$GIT_PROGRAM" log -n 1 "origin/${branch}" -- "$private_dir" 2>/dev/null)
if [ -n "$found_log" ]; then if [ -n "$found_log" ]; then
debug "Private directory $private_dir is tracked by repo" debug "Private directory $private_dir is tracked by repo"
assert_private_dirs "$private_dir" assert_private_dirs "$private_dir"
fi fi
done done
[ -n "$DEBUG" ] && display_private_perms "pre-merge" [ -n "$DEBUG" ] && display_private_perms "pre-merge"
debug "Doing an initial merge of origin/master" debug "Doing an initial merge of origin/${branch}"
"$GIT_PROGRAM" merge origin/master || { "$GIT_PROGRAM" merge "origin/${branch}" || {
debug "Merge failed, doing a reset and stashing conflicts." debug "Merge failed, doing a reset and stashing conflicts."
"$GIT_PROGRAM" reset origin/master "$GIT_PROGRAM" reset "origin/${branch}"
if cd "$YADM_WORK"; then # necessary because of a bug in Git if cd "$YADM_WORK"; then # necessary because of a bug in Git
"$GIT_PROGRAM" -c user.name='yadm clone' -c user.email='yadm' stash save Conflicts preserved from yadm clone command 2>&1 "$GIT_PROGRAM" -c user.name='yadm clone' -c user.email='yadm' stash save Conflicts preserved from yadm clone command 2>&1
cat <<EOF cat <<EOF
**NOTE** **NOTE**
Merging origin/master failed. Merging origin/${branch} failed.
As a result, yadm did 'reset origin/master', and then As a result, yadm did 'reset origin/${branch}', and then
stashed the conflicting data. stashed the conflicting data.
This likely happened because you had files in \$HOME This likely happened because you had files in \$HOME
which conflicted with files tracked by origin/master. which conflicted with files tracked by origin/${branch}.
You can review the stashed conflicts with the You can review the stashed conflicts with the
command 'yadm stash show -p' from within your command 'yadm stash show -p' from within your
@ -685,15 +700,15 @@ EOF
DO_BOOTSTRAP=0 DO_BOOTSTRAP=0
cat <<EOF cat <<EOF
**NOTE** **NOTE**
Merging origin/master failed. Merging origin/${branch} failed.
yadm did 'reset origin/master' instead. yadm did 'reset origin/${branch}' instead.
yadm did not stash these conflicts beacuse it was unable yadm did not stash these conflicts beacuse it was unable
to change to the $YADM_WORK directory. to change to the $YADM_WORK directory.
Please review and resolve any differences appropriately Please review and resolve any differences appropriately
If you know what you're doing, and want to overwrite the If you know what you're doing, and want to overwrite the
tracked files, consider 'yadm reset --hard origin/master' tracked files, consider 'yadm reset --hard origin/${branch}'
EOF EOF
fi fi
} }
@ -1036,6 +1051,17 @@ function version() {
# ****** Utility Functions ****** # ****** Utility Functions ******
function is_valid_branch_name() {
# Git branches do not allow:
# * path component that begins with "."
# * double dot
# * "~", "^", ":", "\", space
# * end with a "/"
# * end with ".lock"
[[ "$1" =~ (\/\.|\.\.|[~^:\\ ]|\/$|\.lock$) ]] && return 1
return 0
}
function query_distro() { function query_distro() {
distro="" distro=""
if command -v "$LSB_RELEASE_PROGRAM" >/dev/null 2>&1; then if command -v "$LSB_RELEASE_PROGRAM" >/dev/null 2>&1; then

10
yadm.1
View File

@ -19,13 +19,15 @@ yadm \- Yet Another Dotfiles Manager
init init
.RB [ -f ] .RB [ -f ]
.RB [ -w .RB [ -w
.IR directory ] .IR dir ]
.B yadm .B yadm
.RI clone " url .RI clone " url
.RB [ -f ] .RB [ -f ]
.RB [ -w .RB [ -w
.IR directory ] .IR dir ]
.RB [ -b
.IR branch ]
.RB [ --bootstrap ] .RB [ --bootstrap ]
.RB [ --no-bootstrap ] .RB [ --no-bootstrap ]
@ -146,6 +148,10 @@ 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
.IR origin/master ,
you can specify it using the
.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
exists). The options exists). The options
.BR --bootstrap " or " --no-bootstrap .BR --bootstrap " or " --no-bootstrap