From f3bde37f785fa9ea5782e6a155e66fdbc3e4064e Mon Sep 17 00:00:00 2001 From: Tim Byrne Date: Mon, 7 Oct 2019 08:36:32 -0500 Subject: [PATCH] Support `-b ` when cloning (#133) --- completion/yadm.bash_completion | 2 +- test/test_clone.py | 45 +++++++++++++++++++++ test/test_unit_is_valid_branch_name.py | 40 +++++++++++++++++++ yadm | 54 +++++++++++++++++++------- yadm.1 | 10 ++++- 5 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 test/test_unit_is_valid_branch_name.py diff --git a/completion/yadm.bash_completion b/completion/yadm.bash_completion index 1091e55..0f21da8 100644 --- a/completion/yadm.bash_completion +++ b/completion/yadm.bash_completion @@ -55,7 +55,7 @@ if declare -F _git > /dev/null; then case "$antepenultimate" in clone) - COMPREPLY=( $(compgen -W "-f -w --bootstrap --no-bootstrap" -- "$current") ) + COMPREPLY=( $(compgen -W "-f -w -b --bootstrap --no-bootstrap" -- "$current") ) return 0 ;; esac diff --git a/test/test_clone.py b/test/test_clone.py index 231d967..1650e92 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -251,6 +251,51 @@ def test_clone_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): """Assert clone is successful""" assert run.code == expected_code diff --git a/test/test_unit_is_valid_branch_name.py b/test/test_unit_is_valid_branch_name.py new file mode 100644 index 0000000..9e5b6d1 --- /dev/null +++ b/test/test_unit_is_valid_branch_name.py @@ -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 diff --git a/yadm b/yadm index 656e508..fc5fa29 100755 --- a/yadm +++ b/yadm @@ -610,11 +610,20 @@ function clean() { function clone() { DO_BOOTSTRAP=1 + local branch + branch="master" clone_args=() while [[ $# -gt 0 ]] ; do key="$1" 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 DO_BOOTSTRAP=2 ;; @@ -634,12 +643,12 @@ function clone() { local 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" "$GIT_PROGRAM" remote add origin "${clone_args[@]}" - debug "Configuring new repo to track origin/master" - "$GIT_PROGRAM" config branch.master.remote origin - "$GIT_PROGRAM" config branch.master.merge refs/heads/master + debug "Configuring new repo to track origin/${branch}" + "$GIT_PROGRAM" config "branch.${branch}.remote" origin + "$GIT_PROGRAM" config "branch.${branch}.merge" "refs/heads/${branch}" # fetch / merge (and possibly fallback to reset) debug "Doing an initial fetch of the origin" @@ -648,30 +657,36 @@ function clone() { rm -rf "$YADM_REPO" 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" 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 debug "Private directory $private_dir is tracked by repo" assert_private_dirs "$private_dir" fi done [ -n "$DEBUG" ] && display_private_perms "pre-merge" - debug "Doing an initial merge of origin/master" - "$GIT_PROGRAM" merge origin/master || { + debug "Doing an initial merge of origin/${branch}" + "$GIT_PROGRAM" merge "origin/${branch}" || { 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 "$GIT_PROGRAM" -c user.name='yadm clone' -c user.email='yadm' stash save Conflicts preserved from yadm clone command 2>&1 cat </dev/null 2>&1; then diff --git a/yadm.1 b/yadm.1 index 526b6f9..a1b5be5 100644 --- a/yadm.1 +++ b/yadm.1 @@ -19,13 +19,15 @@ yadm \- Yet Another Dotfiles Manager init .RB [ -f ] .RB [ -w -.IR directory ] +.IR dir ] .B yadm .RI clone " url .RB [ -f ] .RB [ -w -.IR directory ] +.IR dir ] +.RB [ -b +.IR branch ] .RB [ --bootstrap ] .RB [ --no-bootstrap ] @@ -146,6 +148,10 @@ but this can be overridden with the .BR -w " option. yadm can be forced to overwrite an existing repository by providing the .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 exists). The options .BR --bootstrap " or " --no-bootstrap