diff --git a/Commands.md b/Commands.md index e9af2477..0c6ddbf2 100644 --- a/Commands.md +++ b/Commands.md @@ -1203,6 +1203,14 @@ Internally this script uses `rsync` and not `scp` as the name suggests. `git-rscp` - The reverse of `git-scp`. Copies specific files from the working directory of a remote repository to the current working directory. +The following options are available (must precede ``): + +```bash + -v, --verbose Print what will be synced before syncing + -i, --interactive Prompt for confirmation before syncing (implies --verbose) + -n, --dry-run Show what would be synced, change nothing (implies --verbose) +``` + ### Examples Copy unstaged files to remote. Useful when you want to make quick test without making any commits diff --git a/bin/git-scp b/bin/git-scp index 7a10a1fe..0c0013c0 100755 --- a/bin/git-scp +++ b/bin/git-scp @@ -77,6 +77,17 @@ function _sanitize() function scp_and_stage { + local verbose=0 interactive=0 dry_run=0 + while : + do + case "$1" in + -v|--verbose) verbose=1; shift;; + -i|--interactive) verbose=1; interactive=1; shift;; + -n|--dry-run) verbose=1; dry_run=1; shift;; + *) break;; + esac + done + set_remote "$1" shift @@ -94,46 +105,116 @@ function scp_and_stage list=$(git ls-files "$@")" "$(git ls-files -o "$@") elif [ -n "$refhead" ] then - git diff --stat "$refhead" + [ "$verbose" -eq 1 ] && git --no-pager diff --stat "$refhead" list=$(git diff "$refhead" --name-only) else - git diff + [ "$verbose" -eq 1 ] && git --no-pager diff list=$(git diff --name-only) fi deleted=$(for i in $list; do [ -f "$i" ] || echo "$i"; done) list=$(for i in $list; do [ -f "$i" ] && echo "$i"; done) + if [ "$interactive" -eq 1 ] && [ "$dry_run" -eq 0 ] && { [ -n "$list" ] || [ -n "$deleted" ]; } + then + local reply + read -r -p "Proceed with sync to $remote? [y/N] " reply + case "$reply" in + y|Y|yes|YES) ;; + *) _info "Aborted, nothing synced."; exit 0;; + esac + fi + + local status=0 + if [ -n "$list" ] then local _TMP=${0///} # shellcheck disable=SC2086 - echo "$list" > "$_TMP" && - _sanitize $list && - _info "Pushing to $remote ($(git config "remote.$remote.url"))" && - rsync -rlDv --files-from="$_TMP" ./ "$(git config "remote.$remote.url")/" && - git add --force $list && + echo "$list" > "$_TMP" + if [ "$dry_run" -eq 1 ] || _sanitize $list + then + _info "Pushing to $remote ($(git config "remote.$remote.url"))" + if [ "$dry_run" -eq 1 ] + then + rsync -rlDvn --files-from="$_TMP" ./ "$(git config "remote.$remote.url")/" + else + rsync -rlDv --files-from="$_TMP" ./ "$(git config "remote.$remote.url")/" && + git add --force $list + fi + status=$? + else + status=1 + fi rm "$_TMP" fi deleted=$(for i in $deleted; do echo "$(git config "remote.$remote.url" | cut -d: -f2)/$i"; done) - [ -n "$deleted" ] && - COLOR_RED && - echo Deleted remote files && - ssh "$(git config "remote.$remote.url" | cut -d: -f1)" -t "rm $deleted" && - echo "$deleted" - COLOR_RESET + if [ -n "$deleted" ] + then + COLOR_RED + echo Deleted remote files + if [ "$dry_run" -eq 1 ] + then + echo "$deleted" + else + if ssh "$(git config "remote.$remote.url" | cut -d: -f1)" -t "rm $deleted" + then + echo "$deleted" + else + status=1 + fi + fi + COLOR_RESET + fi + + return "$status" } function reverse_scp() { + local verbose=0 interactive=0 dry_run=0 + while : + do + case "$1" in + -v|--verbose) verbose=1; shift;; + -i|--interactive) verbose=1; interactive=1; shift;; + -n|--dry-run) verbose=1; dry_run=1; shift;; + *) break;; + esac + done + set_remote "$1" shift local _TMP=${0///} - echo "$@" > "$_TMP" && - rsync -rlDv --files-from="$_TMP" "$(git config "remote.$remote.url")/" ./ && + echo "$@" > "$_TMP" + + local status=0 + if [ "$verbose" -eq 1 ] + then + rsync -rlDvn --files-from="$_TMP" "$(git config "remote.$remote.url")/" ./ + status=$? + fi + + if [ "$dry_run" -eq 1 ] + then + rm "$_TMP" + return "$status" + fi + + if [ "$interactive" -eq 1 ] + then + local reply + read -r -p "Proceed with copy from $remote? [y/N] " reply + case "$reply" in + y|Y|yes|YES) ;; + *) _info "Aborted, nothing copied."; rm "$_TMP"; exit 0;; + esac + fi + + rsync -rlDv --files-from="$_TMP" "$(git config "remote.$remote.url")/" ./ && rm "$_TMP" } @@ -148,9 +229,14 @@ function _usage() { echo "Usage: git scp -h|help|? - git scp [ref|file..] # scp and stage your files to specified remote - git scp [] # show diff relative to and upload unstaged files to - git rscp [] # copy files to current working directory + git scp [options] [ref|file..] # scp and stage your files to specified remote + git scp [options] [] # show diff relative to and upload unstaged files to + git rscp [options] [] # copy files to current working directory + + OPTIONS: + -v, --verbose print what will be synced before syncing + -i, --interactive prompt for confirmation before syncing (implies --verbose) + -n, --dry-run show what would be synced, change nothing (implies --verbose; no staging, no remote writes/deletes) " case $1 in diff --git a/man/git-scp.md b/man/git-scp.md index cb63f934..f1a096e6 100644 --- a/man/git-scp.md +++ b/man/git-scp.md @@ -4,8 +4,8 @@ git-scp(1) -- Copy files to SSH compatible `git-remote` ## SYNOPSIS `git scp` -h|help|? - `git scp` [...|...] - `git rscp` + `git scp` [options] [...|...] + `git rscp` [options] ## DESCRIPTION @@ -29,6 +29,18 @@ Internally this script uses `rsync` and not `scp` as the name suggests. The parameters, when given, are used to limit the diff to the named paths (you can give directory names and get diff for all files under them). + -v, --verbose + + Print what will be synced before syncing: the diff for `git scp`, or a preview (rsync dry-run) for `git rscp`. + + -i, --interactive + + Same as `--verbose`, but also prompt for confirmation before syncing. Implies `--verbose` + + -n, --dry-run + + Show what would be synced, change nothing. Implies `--verbose`, and no staging or remote writes/deletes happen. + ## GIT CONFIGS To sanitize files using `dos2unix` before copying files diff --git a/tests/git-scp.bats b/tests/git-scp.bats new file mode 100644 index 00000000..23930a03 --- /dev/null +++ b/tests/git-scp.bats @@ -0,0 +1,46 @@ +# shellcheck shell=bash + +source "$BATS_TEST_DIRNAME/test_util.sh" + +setup_file() { + test_util.setup_file +} + +setup() { + test_util.cd_test + + test_util.git_init + printf '%s\n' 'hello' > tracked.txt + git add tracked.txt + git commit -m 'Initial commit' + + # never created: neither test below should ever touch it + git remote add fake "$BATS_TEST_TMPDIR/never-created" + + printf '%s\n' 'world' >> tracked.txt +} + +@test "dry-run previews the sync without changing anything" { + run git scp -n fake + assert_success + assert_line -p 'tracked.txt' + assert_line -p 'DRY RUN' + + run git status --short + assert_line -p 'M tracked.txt' + + run test -e "$BATS_TEST_TMPDIR/never-created" + assert_failure +} + +@test "interactive mode aborts without syncing when declined" { + run bash -c 'echo n | git scp -i fake' + assert_success + assert_line -p 'Aborted, nothing synced.' + + run git status --short + assert_line -p 'M tracked.txt' + + run test -e "$BATS_TEST_TMPDIR/never-created" + assert_failure +}