From aef6cc1f48437621cf4b8b02612a99197b79281b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AJ=20C=C3=B4t=C3=A9?= <57828010+anderewrey@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:32:42 -0400 Subject: [PATCH 1/5] feat(scp): add -v/--verbose and -i/--interactive flags git scp/git rscp printed a diff via plain `git diff`/`git diff --stat` with no `--no-pager` and no TTY check, blocking on the user's pager. Add -v/--verbose to print the diff via --no-pager before syncing, and -i/--interactive to also prompt for confirmation before the real push/copy. Both apply to git scp and git rscp, and must precede . Updates man/git-scp.md and Commands.md. --- Commands.md | 7 ++++++ bin/git-scp | 64 ++++++++++++++++++++++++++++++++++++++++++++------ man/git-scp.md | 12 ++++++++-- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/Commands.md b/Commands.md index e9af2477..a06a4ede 100644 --- a/Commands.md +++ b/Commands.md @@ -1203,6 +1203,13 @@ 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) +``` + ### 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..3ecc8933 100755 --- a/bin/git-scp +++ b/bin/git-scp @@ -77,6 +77,16 @@ function _sanitize() function scp_and_stage { + local verbose=0 interactive=0 + while : + do + case "$1" in + -v|--verbose) verbose=1; shift;; + -i|--interactive) verbose=1; interactive=1; shift;; + *) break;; + esac + done + set_remote "$1" shift @@ -94,16 +104,26 @@ 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 ] && { [ -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 + if [ -n "$list" ] then local _TMP=${0///} @@ -128,12 +148,38 @@ function scp_and_stage function reverse_scp() { + local verbose=0 interactive=0 + while : + do + case "$1" in + -v|--verbose) verbose=1; shift;; + -i|--interactive) verbose=1; interactive=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" + + if [ "$verbose" -eq 1 ] + then + rsync -rlDvni --files-from="$_TMP" "$(git config "remote.$remote.url")/" ./ + 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 +194,13 @@ 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) " case $1 in diff --git a/man/git-scp.md b/man/git-scp.md index cb63f934..6e06fcb8 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,14 @@ 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` + ## GIT CONFIGS To sanitize files using `dos2unix` before copying files From 37706bcb41c0ee913a6a0597c4d9ff227b9c07d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AJ=20C=C3=B4t=C3=A9?= <57828010+anderewrey@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:32:42 -0400 Subject: [PATCH 2/5] feat(scp): add -n/--dry-run flag Add -n/--dry-run to preview a sync without changing anything: rsync runs with -n, and git add --force / ssh ... rm are skipped outright. Implies --verbose, since confirming or previewing with nothing shown is useless. Also restructures scp_and_stage's push/delete blocks from `&&`-chains into if/then, needed to branch on dry-run, which incidentally fixes a latent bug where a non-TTY COLOR_RESET could silently skip the real rsync/git add/ssh rm calls. Updates man/git-scp.md and Commands.md. --- Commands.md | 1 + bin/git-scp | 51 ++++++++++++++++++++++++++++++++++++-------------- man/git-scp.md | 4 ++++ 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/Commands.md b/Commands.md index a06a4ede..0c6ddbf2 100644 --- a/Commands.md +++ b/Commands.md @@ -1208,6 +1208,7 @@ 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 diff --git a/bin/git-scp b/bin/git-scp index 3ecc8933..8efcb86c 100755 --- a/bin/git-scp +++ b/bin/git-scp @@ -77,12 +77,13 @@ function _sanitize() function scp_and_stage { - local verbose=0 interactive=0 + 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 @@ -114,7 +115,7 @@ function scp_and_stage 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 ] && { [ -n "$list" ] || [ -n "$deleted" ]; } + 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 @@ -128,32 +129,47 @@ function scp_and_stage 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 _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 + 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 + ssh "$(git config "remote.$remote.url" | cut -d: -f1)" -t "rm $deleted" && + echo "$deleted" + fi + COLOR_RESET + fi } function reverse_scp() { - local verbose=0 interactive=0 + 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 @@ -169,6 +185,12 @@ function reverse_scp() rsync -rlDvni --files-from="$_TMP" "$(git config "remote.$remote.url")/" ./ fi + if [ "$dry_run" -eq 1 ] + then + rm "$_TMP" + return 0 + fi + if [ "$interactive" -eq 1 ] then local reply @@ -201,6 +223,7 @@ function _usage() 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 6e06fcb8..f1a096e6 100644 --- a/man/git-scp.md +++ b/man/git-scp.md @@ -37,6 +37,10 @@ Internally this script uses `rsync` and not `scp` as the name suggests. 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 From e83c9825ac91b0eed4afd0df6cff7f2e9cdd1273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AJ=20C=C3=B4t=C3=A9?= <57828010+anderewrey@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:53:06 -0400 Subject: [PATCH 3/5] test(scp): add coverage for -n/--dry-run and -i/--interactive abort git-scp had no test coverage at all. Cover the two paths that need no real remote destination: dry-run must preview without touching the index, working tree, or remote path, and declining the interactive confirmation must abort before anything syncs. --- tests/git-scp.bats | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/git-scp.bats 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 +} From 220e5120db5c5c3af570b8d98f6d3792959a6b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AJ=20C=C3=B4t=C3=A9?= <57828010+anderewrey@users.noreply.github.com> Date: Tue, 7 Jul 2026 06:45:48 -0400 Subject: [PATCH 4/5] feat(scp): Code review --- bin/git-scp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/bin/git-scp b/bin/git-scp index 8efcb86c..995af57d 100755 --- a/bin/git-scp +++ b/bin/git-scp @@ -125,12 +125,14 @@ function scp_and_stage esac fi + local status=0 + if [ -n "$list" ] then local _TMP=${0///} # shellcheck disable=SC2086 echo "$list" > "$_TMP" - if _sanitize $list + if [ "$dry_run" -eq 1 ] || _sanitize $list then _info "Pushing to $remote ($(git config "remote.$remote.url"))" if [ "$dry_run" -eq 1 ] @@ -140,6 +142,9 @@ function scp_and_stage rsync -rlDv --files-from="$_TMP" ./ "$(git config "remote.$remote.url")/" && git add --force $list fi + status=$? + else + status=1 fi rm "$_TMP" fi @@ -154,11 +159,17 @@ function scp_and_stage then echo "$deleted" else - ssh "$(git config "remote.$remote.url" | cut -d: -f1)" -t "rm $deleted" && - echo "$deleted" + 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() @@ -180,15 +191,17 @@ function reverse_scp() local _TMP=${0///} echo "$@" > "$_TMP" + local status=0 if [ "$verbose" -eq 1 ] then rsync -rlDvni --files-from="$_TMP" "$(git config "remote.$remote.url")/" ./ + status=$? fi if [ "$dry_run" -eq 1 ] then rm "$_TMP" - return 0 + return "$status" fi if [ "$interactive" -eq 1 ] From eab4b0e716793533091bff46b3a5cfe925e01fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AJ=20C=C3=B4t=C3=A9?= <57828010+anderewrey@users.noreply.github.com> Date: Thu, 9 Jul 2026 07:02:35 -0400 Subject: [PATCH 5/5] feat(scp): Code review --- bin/git-scp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/git-scp b/bin/git-scp index 995af57d..0c0013c0 100755 --- a/bin/git-scp +++ b/bin/git-scp @@ -194,7 +194,7 @@ function reverse_scp() local status=0 if [ "$verbose" -eq 1 ] then - rsync -rlDvni --files-from="$_TMP" "$(git config "remote.$remote.url")/" ./ + rsync -rlDvn --files-from="$_TMP" "$(git config "remote.$remote.url")/" ./ status=$? fi