Last active
May 21, 2018 01:10
-
-
Save netj/54c8849681c13f11a4ec50d04041e0f3 to your computer and use it in GitHub Desktop.
MOVED TO: https://github.com/netj/remocon since 2018-05
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# remocon -- run given command remotely, replicating local git work tree on a remote host, and downloading remote changes if needed | |
# | |
# Author: Jaeho Shin <[email protected]> | |
# Created: 2018-03-08 | |
## | |
set -euo pipefail | |
error() { echo >&2 "📡 ‼️ " "$@"; false; } | |
warning() { echo >&2 "📡 ⚠️ " "$@"; } | |
info() { echo >&2 "📡" "$@"; } | |
################################################################################ | |
# a nimble/simple way to use bash -x itself to get single-quoted escapes instead of backslashes given by printf %q | |
@q() { | |
local e | |
e=$(PS4= bash --norc -xc ': "$@"' -- "$@" 2>&1) | |
echo "${e:2}" | |
} | |
# a handy way to show what command is running | |
x() { | |
( | |
case ${1:-} in (builtin|command) shift;; esac | |
echo "$PS4$(@q "$@")" | |
) >&2 | |
"$@" | |
} | |
mkdelegate() { | |
: echo 'generates an executable file for delegating with extra options' | |
: echo | |
: echo 'mkdelegate FILE ABS_PATH_TO_COMMAND [OPTION...]' | |
local file=$1; shift | |
mkdir -p "$(dirname "$file")" | |
local script=$( | |
echo '#!/bin/sh' | |
echo "$(@q exec "$@")" '"$@"' | |
) | |
diff -q <(echo "$script") "$file" &>/dev/null || echo "$script" >"$file" | |
chmod +x "$file" | |
} | |
# a handy way to patch remote | |
# See: https://github.com/netj/bpm/blob/master/plugin/git-helper | |
git-tether-remote() { | |
( | |
set -euo pipefail | |
hostpath=$1; shift | |
case $hostpath in | |
(*:*) host=${hostpath%%:*} dir=${hostpath#*:};; | |
(*) error "$hostpath: HOST[:PATH] required";; | |
esac | |
commit=$(git rev-parse HEAD) | |
branch=$(git symbolic-ref --short HEAD) | |
git remote add remocon/remote "$host:$dir" 2>/dev/null || | |
git remote set-url remocon/remote "$host:$dir" | |
git push -q -f remocon/remote HEAD:"$branch" 2>/dev/null || { | |
ssh "$host" " | |
set -eu; PS4='++ '; # set -x | |
mkdir -p $(@q "$dir") | |
cd $(@q "$dir") | |
[[ -e .git ]] || git init | |
# lift some git config to allow push | |
git config receive.denyCurrentBranch ignore | |
" | |
git push -f remocon/remote HEAD:"$branch" | |
} | |
# TODO transfer git config to destination | |
git remote remove remocon/remote | |
# bring destination to the current commit | |
ssh "$host" "set -eu; PS4='++ '; # set -x | |
cd $(@q "$dir") | |
branch=$(@q "$branch") | |
commit=$(@q "$commit") | |
# reverse any previous patch for tethering | |
if [[ -s .git/tethered.patch ]]; then | |
git apply --binary -R <.git/tethered.patch || git stash | |
mv -f .git/tethered.patch{,~} | |
fi | |
if git rev-parse HEAD &>/dev/null; then | |
# preserve any outstanding/untethered changes | |
git diff --quiet --exit-code HEAD -- || git stash | |
# make sure we're on the tethered branch and commit | |
[[ \$(git symbolic-ref --short HEAD) = \$branch ]] || git checkout -f \$branch -- | |
else | |
git checkout -f \$branch -- | |
fi | |
git reset --hard \$commit | |
" | |
# send staged and unstaged changes | |
git diff --full-index --binary HEAD | | |
ssh "$host" "cat >$(@q "$dir")/.git/tethered.patch" | |
ssh "$host" "set -eu; PS4='++ '; # set -x | |
cd $(@q "$dir") | |
# with the same outstanding changes on top of the current commit | |
! [[ -s .git/tethered.patch ]] || git apply --binary --apply --stat --cached <.git/tethered.patch | |
git checkout --quiet . | |
git reset --quiet | |
" | |
# replicate staged changes AKA .git/index | |
git diff --full-index --binary --cached | | |
ssh "$host" "cat >$(@q "$dir")/.git/tethered-index.patch" | |
ssh "$host" "set -eu; PS4='++ '; # set -x | |
cd $(@q "$dir") | |
! [[ -s .git/tethered-index.patch ]] || git apply --binary --apply --cached <.git/tethered-index.patch | |
" | |
) | |
} | |
################################################################################ | |
# common prep and sub-commands | |
{ | |
# make sure we're in a git work tree | |
$(git rev-parse --is-inside-work-tree) || | |
error "$PWD: Not inside a git work tree" | |
# find closest .remocon.conf | |
conf=$( | |
until [[ $PWD = / || -e .remocon.conf ]]; do cd ..; done | |
! [[ -e .remocon.conf ]] || echo "$PWD"/.remocon.conf | |
) | |
# defaults to not running things remotely | |
ssh_opts=( | |
) | |
bash_opts=( | |
bash | |
) | |
! [[ -e "$conf" ]] || source "$conf" | |
: ${remote:=localhost} | |
# parse remote | |
remote_host=${remote%%:*} | |
remote_repo_root=${remote#$remote_host} | |
remote_repo_root=${remote_repo_root#:} | |
# use local git work tree's basename and keep it under given remote_repo_root dir | |
remote_repo=${remote_repo_root:+$remote_repo_root/}$(basename "$(git rev-parse --show-toplevel)") | |
# determine remote workdir based on where in the git repo we're in | |
local_path_within_git=$(git rev-parse --show-prefix) | |
remote_workdir="${remote_repo}/${local_path_within_git#/}" | |
# override ssh options/config | |
sshBoosterOpts=( | |
# share an ssh connection across invocation | |
-o ControlMaster=auto | |
-o ControlPath="/tmp/remocon-$USER.sock-%r@%h:%p" | |
-o ControlPersist=600 | |
# forward agent | |
-A | |
) | |
sshBoosterRoot=~/.cache/remocon/ssh | |
for cmd in scp ssh; do | |
mkdelegate "$sshBoosterRoot"/bin/"$cmd" "$(type -p "$cmd")" "${sshBoosterOpts[@]}" | |
done | |
PATH="$sshBoosterRoot"/bin:"$PATH" | |
} </dev/null >&2 | |
# tether remote git repo to local one | |
remocon.put() { | |
[[ $# -eq 0 ]] || error "Cannot put partial changes under given paths: $(@q "$@")" | |
info "[$remote_host:$remote_repo/] 🛰 putting a replica of local git work tree on remote" | |
git-tether-remote "$remote_host:$remote_repo" | |
} </dev/null >&2 | |
# put and run given command on remote from the same workdir relative to the git top-level (AKA git prefix) | |
remocon.run() { | |
remocon.put | |
[[ $# -gt 0 ]] || set -- bash -il | |
info "[$remote_host:$remote_workdir] ⚡️ running command: $(@q "$@")" | |
case $remote_host in | |
localhost) | |
# just run given command when remote is local | |
warning "[$remote_host] Not running remotely" | |
x "$@" | |
;; | |
*) | |
if [[ -t 0 && -t 1 && -t 2 ]]; then | |
# when I/O/Err is a fully functional terminal | |
ssh_opts+=(-t) # ask ssh for tty | |
bash_opts+=(-i) # ask bash for an interactive shell | |
fi | |
x ssh "$remote_host" \ | |
"${ssh_opts[@]:---}" \ | |
"$(@q "${bash_opts[@]}" -c "cd $(@q "$remote_workdir") && exec $(@q "$@")")" | |
esac | |
} | |
# get remote changes back to local | |
remocon.get() { | |
[[ $# -gt 0 ]] || set -- . | |
info "[$remote_host:$remote_workdir] 💎 getting remote files under $# paths: $(@q "$@")" | |
# TODO use git in case rsync is not available? | |
x rsync \ | |
--verbose \ | |
--archive \ | |
--hard-links \ | |
--omit-dir-times \ | |
--checksum \ | |
--copy-unsafe-links \ | |
--exclude=.git \ | |
--relative --rsync-path="$(printf 'cd %q &>/dev/null && rsync' "$remote_workdir")" \ | |
"$remote_host":"$(@q "$@")" . | |
} </dev/null >&2 | |
# programming round-trip mode (put-run-get) | |
remocon.prg() { | |
# find which paths to get from given args | |
# (NOTE path list can be terminated by a double-dash `--` to delinate the command to run) | |
local pathsToPull= | |
pathsToPull=() | |
while [[ $# -gt 0 ]]; do | |
local arg=$1; shift | |
case $arg in | |
--) break ;; | |
*) pathsToPull+=("$arg") | |
esac | |
done | |
# run command if any were given (after a dash-dash) | |
local exitStatus=0 | |
[[ $# -eq 0 ]] || remocon.run "$@" || exitStatus=$? | |
# then get files | |
set --; [[ ${#pathsToPull[@]} -eq 0 ]] || set -- "${pathsToPull[@]}" | |
remocon.get "$@" || exitStatus=$? | |
return $exitStatus | |
} | |
################################################################################ | |
# dispatch sub-commands | |
if ! [[ $# -gt 0 ]]; then | |
if [[ -t 0 && -t 1 && -t 2 ]]; then | |
# in a tty, defaults to replicating and opening an interactive/login shell on remote | |
set -- run | |
else | |
# otherwise, defaults to just replicating local git work tree to remote | |
set -- put | |
fi | |
fi | |
cmd=$1; shift | |
handler="remocon.$cmd" | |
type "$handler" &>/dev/null || | |
error "$cmd: No such command. Command must be one of: get, put, run, prg" | |
"$handler" "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment