diff --git a/git-open b/git-open index cc5b8b8..e009116 100755 --- a/git-open +++ b/git-open @@ -56,12 +56,51 @@ if [[ -z "$giturl" ]]; then exit 1 fi +ssh_config=${ssh_config:-~/.ssh/config} +# Resolves an ssh alias defined in ssh_config to it's corresponding hostname +# echos out result, should be used within subshell $( ssh_resolve $host ) +# echos out nothing if alias could not be resolved +function ssh_resolve() { + domain="$1" + ssh_found=true + # Filter to only ssh_config lines that start with "Host" or "HostName" + resolved=$(while read -r ssh_line; do + # Split each line by spaces, of the form: + # Host alias [alias...] + # Host regex + # HostName resolved.domain.com + read -r -a ssh_array <<<"${ssh_line}" + ssh_optcode="${ssh_array[0]}" + if [[ ${ssh_optcode^^} == HOST ]]; then + # Host + ssh_found=false + # Iterate through aliases looking for a match + for ssh_index in $(seq 1 $((${#ssh_array[@]} - 1))); do + ssh_host=${ssh_array[$ssh_index]} + # shellcheck disable=SC2053 + if [[ $domain == $ssh_host ]]; then + # Found a match, next HostName entry will be returned while matched + ssh_found=true + break + fi + done + elif $ssh_found && [[ ${ssh_optcode^^} == HOSTNAME ]]; then + # HostName, but only if ssh_found is true (the last Host entry matched) + # Replace all instances of %h with the Host alias + echo "${ssh_array[1]//%h/$domain}" + fi + done < <(grep -iE "^\\s*Host(Name)?\\s+" "$ssh_config")) + # Take only the last resolved hostname (multiple are overridden) + tail -1 <<<"$resolved" +} + # From git-fetch(5), native protocols: # ssh://[user@]host.xz[:port]/path/to/repo.git/ # git://host.xz[:port]/path/to/repo.git/ # http[s]://host.xz[:port]/path/to/repo.git/ # ftp[s]://host.xz[:port]/path/to/repo.git/ # [user@]host.xz:path/to/repo.git/ - scp-like but is an alternative to ssh. +# [user@]hostalias:path/to/repo.git/ - handles host aliases defined in ssh_config(5) # Determine whether this is a url (https, ssh, git+ssh...) or an scp-style path if [[ "$giturl" =~ ^[a-z\+]+://.* ]]; then @@ -85,6 +124,14 @@ else # Split on first ':' to get server name and path domain=${uri%%:*} urlpath=${uri#*:} + + # Resolve sshconfig aliases + if [[ -e "$ssh_config" ]]; then + domain_resolv=$(ssh_resolve "$domain") + if [[ ! -z "$domain_resolv" ]]; then + domain="$domain_resolv" + fi + fi fi # Trim "/" from beginning of URL; "/" and ".git" from end of URL @@ -171,7 +218,9 @@ case $( uname -s ) in esac # Allow printing the url if BROWSER=echo -if [[ $BROWSER != "echo" ]]; then +if [[ $BROWSER == "echo" ]]; then + openopt='' +else exec &>/dev/null fi diff --git a/test/git-open.bats b/test/git-open.bats index 1577a67..76507b6 100755 --- a/test/git-open.bats +++ b/test/git-open.bats @@ -174,6 +174,113 @@ setup() { assert_output "http://github.com/user/repo" } +## +## SSH config +## + +@test "sshconfig: basic" { + create_ssh_sandbox + # Basic + git remote set-url origin "basic:user/repo.git" + run ../git-open + assert_output --partial "https://basic.com/user/repo" + # With git user + git remote set-url origin "git@nouser:user/repo.git" + run ../git-open + assert_output "https://no.user/user/repo" +} + +@test "sshconfig: no action on no match" { + create_ssh_sandbox + git remote set-url origin "git@nomatch:user/repo.git" + run ../git-open + assert_output "https://nomatch/user/repo" + # No match due to improper casing +} + +@test "sshconfig: check case sensitivity" { + create_ssh_sandbox + # Host and HostName keywords should be case insensitive + # But output URL will be case sensitive + git remote set-url origin "malformed:user/repo.git" + run ../git-open + assert_output "https://MaL.FoRmEd/user/repo" + # SSH aliases (hosts) are case sensitive, this should not match + git remote set-url origin "git@MALFORMED:user/repo.git" + run ../git-open + refute_output "https://MaL.FoRmEd/user/repo" +} + +@test "sshconfig: multitarget host" { + create_ssh_sandbox + for i in $(seq 1 3); do + git remote set-url origin "multi$i:user/repo.git" + run ../git-open + assert_output "https://multi.com/user/repo" + done +} + +@test "sshconfig: host substitution in hostname" { + create_ssh_sandbox + for i in $(seq 1 3); do + git remote set-url origin "sub$i:user/repo.git" + run ../git-open + assert_output "https://sub$i.multi.com/user/repo" + done +} + +@test "sshconfig: host wildcard * matches zero or more chars" { + create_ssh_sandbox + # Normal * + for str in "" "-prod" "-dev"; do + git remote set-url origin "zero$str:user/repo.git" + run ../git-open + assert_output "https://zero.com/user/repo" + done + # * with substitution + for str in "" "-prod" "-dev"; do + git remote set-url origin "subzero$str:user/repo.git" + run ../git-open + assert_output "https://subzero$str.zero/user/repo" + done +} + +@test "sshconfig: host wildcard ? matches exactly one char" { + create_ssh_sandbox + # Normal ? + for i in $(seq 1 3); do + git remote set-url origin "one$i:user/repo.git" + run ../git-open + assert_output "https://one.com/user/repo" + done + # Refute invalid match on ? + for str in "" "-test"; do + git remote set-url origin "one:user/repo.git" + run ../git-open + refute_output "https://one$str.com/user/repo" + done + + # ? with substitution + for i in $(seq 1 3); do + git remote set-url origin "subone$i:user/repo.git" + run ../git-open + assert_output "https://subone$i.one/user/repo" + done + # Refute invalid match on ? with substitution + for str in "" "-test"; do + git remote set-url origin "subone$str:user/repo.git" + run ../git-open + refute_output "https://subone$str.one/user/repo" + done + # Refute invalid match on ? with substitution +} + +@test "sshconfig: overriding host rules" { + create_ssh_sandbox + git remote set-url origin "zero-override:user/repo.git" + run ../git-open + assert_output "https://override.zero.com/user/repo" +} ## ## Bitbucket @@ -393,6 +500,9 @@ setup() { teardown() { cd .. rm -rf "$foldername" + rm -rf "$ssh_config" + refute [ -e "$ssh_config" ] + unset ssh_config } # helper to create a test git sandbox that won't dirty the real repo @@ -418,3 +528,65 @@ function create_git_sandbox() { git add readme.txt git commit -m "add file" -q } + +# helper to create test SSH config file +function create_ssh_sandbox() { + export ssh_config=$(mktemp) + refute [ -z "$ssh_config" ] + + # Populate ssh config with test data + echo "$ssh_testdata" >$ssh_config + assert [ -e "$ssh_config" ] +} + +# Test SSH config data +ssh_testdata=" +# Autogenerated test sshconfig for paulirish/git-open BATS tests +# It is safe to delete this file, a new one will be generated each test + +Host basic + HostName basic.com + User git + +Host nomatch + User git + +Host nouser + HostName no.user + +host malformed + hOsTnAmE MaL.FoRmEd + User other + +# Multiple targets +Host multi1 multi2 multi3 + HostName multi.com + User git + +Host sub1 sub2 sub3 + HostName %h.multi.com + User git + + # Wildcard * matching (zero or more characters) +Host zero* + HostName zero.com + User git + +Host subzero* + HostName %h.zero + User git + +# Wildcard ? matching (exactly one character) +Host one? + HostName one.com + User git + +Host subone? + HostName %h.one + User git + +# Overrides rule zero* +Host zero-override + HostName override.zero.com + User git +"