Skip to content

Commit 2ac59e3

Browse files
committed
Adds support for sshconfig expansion
See `man ssh_config` for documentation for sshconfig. Only works on SCP-like remote URL's [user@]hostalias:path/to/repo.git Parses through ~/.ssh/config, looking for a Host entry matching hostalias (bash-like pattern matching supported) and expanding it to the corresponding HostName entry, if applicable.
1 parent 8caa9ad commit 2ac59e3

File tree

2 files changed

+222
-1
lines changed

2 files changed

+222
-1
lines changed

git-open

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,51 @@ if [[ -z "$giturl" ]]; then
5757
exit 1
5858
fi
5959

60+
ssh_config=${ssh_config:-~/.ssh/config}
61+
# Resolves an ssh alias defined in ssh_config to it's corresponding hostname
62+
# echos out result, should be used within subshell $( ssh_resolve $host )
63+
# echos out nothing if alias could not be resolved
64+
function ssh_resolve() {
65+
domain="$1"
66+
ssh_found=true
67+
# Filter to only ssh_config lines that start with "Host" or "HostName"
68+
resolved=$(while read -r ssh_line; do
69+
# Split each line by spaces, of the form:
70+
# Host alias [alias...]
71+
# Host regex
72+
# HostName resolved.domain.com
73+
read -r -a ssh_array <<<"${ssh_line}"
74+
ssh_optcode="${ssh_array[0]}"
75+
if [[ ${ssh_optcode^^} == HOST ]]; then
76+
# Host
77+
ssh_found=false
78+
# Iterate through aliases looking for a match
79+
for ssh_index in $(seq 1 $((${#ssh_array[@]} - 1))); do
80+
ssh_host=${ssh_array[$ssh_index]}
81+
# shellcheck disable=SC2053
82+
if [[ $domain == $ssh_host ]]; then
83+
# Found a match, next HostName entry will be returned while matched
84+
ssh_found=true
85+
break
86+
fi
87+
done
88+
elif $ssh_found && [[ ${ssh_optcode^^} == HOSTNAME ]]; then
89+
# HostName, but only if ssh_found is true (the last Host entry matched)
90+
# Replace all instances of %h with the Host alias
91+
echo "${ssh_array[1]//%h/$domain}"
92+
fi
93+
done < <(grep -iE "^\\s*Host(Name)?\\s+" "$ssh_config"))
94+
# Take only the last resolved hostname (multiple are overridden)
95+
tail -1 <<<"$resolved"
96+
}
97+
6098
# From git-fetch(5), native protocols:
6199
# ssh://[user@]host.xz[:port]/path/to/repo.git/
62100
# git://host.xz[:port]/path/to/repo.git/
63101
# http[s]://host.xz[:port]/path/to/repo.git/
64102
# ftp[s]://host.xz[:port]/path/to/repo.git/
65103
# [user@]host.xz:path/to/repo.git/ - scp-like but is an alternative to ssh.
104+
# [user@]hostalias:path/to/repo.git/ - handles host aliases defined in ssh_config(5)
66105

67106
# Determine whether this is a url (https, ssh, git+ssh...) or an scp-style path
68107
if [[ "$giturl" =~ ^[a-z\+]+://.* ]]; then
@@ -86,6 +125,14 @@ else
86125
# Split on first ':' to get server name and path
87126
domain=${uri%%:*}
88127
urlpath=${uri#*:}
128+
129+
# Resolve sshconfig aliases
130+
if [[ -e "$ssh_config" ]]; then
131+
domain_resolv=$(ssh_resolve "$domain")
132+
if [[ ! -z "$domain_resolv" ]]; then
133+
domain="$domain_resolv"
134+
fi
135+
fi
89136
fi
90137

91138
# Trim "/" from beginning of URL; "/" and ".git" from end of URL
@@ -167,7 +214,9 @@ case $( uname -s ) in
167214
esac
168215

169216
# Allow printing the url if BROWSER=echo
170-
if [[ $BROWSER != "echo" ]]; then
217+
if [[ $BROWSER == "echo" ]]; then
218+
openopt=''
219+
else
171220
exec &>/dev/null
172221
fi
173222

test/git-open.bats

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,113 @@ setup() {
162162
assert_output "http://github.com/user/repo"
163163
}
164164

165+
##
166+
## SSH config
167+
##
168+
169+
@test "sshconfig: basic" {
170+
create_ssh_sandbox
171+
# Basic
172+
git remote set-url origin "basic:user/repo.git"
173+
run ../git-open
174+
assert_output --partial "https://basic.com/user/repo"
175+
# With git user
176+
git remote set-url origin "git@nouser:user/repo.git"
177+
run ../git-open
178+
assert_output "https://no.user/user/repo"
179+
}
180+
181+
@test "sshconfig: no action on no match" {
182+
create_ssh_sandbox
183+
git remote set-url origin "git@nomatch:user/repo.git"
184+
run ../git-open
185+
assert_output "https://nomatch/user/repo"
186+
# No match due to improper casing
187+
}
188+
189+
@test "sshconfig: check case sensitivity" {
190+
create_ssh_sandbox
191+
# Host and HostName keywords should be case insensitive
192+
# But output URL will be case sensitive
193+
git remote set-url origin "malformed:user/repo.git"
194+
run ../git-open
195+
assert_output "https://MaL.FoRmEd/user/repo"
196+
# SSH aliases (hosts) are case sensitive, this should not match
197+
git remote set-url origin "git@MALFORMED:user/repo.git"
198+
run ../git-open
199+
refute_output "https://MaL.FoRmEd/user/repo"
200+
}
201+
202+
@test "sshconfig: multitarget host" {
203+
create_ssh_sandbox
204+
for i in $(seq 1 3); do
205+
git remote set-url origin "multi$i:user/repo.git"
206+
run ../git-open
207+
assert_output "https://multi.com/user/repo"
208+
done
209+
}
210+
211+
@test "sshconfig: host substitution in hostname" {
212+
create_ssh_sandbox
213+
for i in $(seq 1 3); do
214+
git remote set-url origin "sub$i:user/repo.git"
215+
run ../git-open
216+
assert_output "https://sub$i.multi.com/user/repo"
217+
done
218+
}
219+
220+
@test "sshconfig: host wildcard * matches zero or more chars" {
221+
create_ssh_sandbox
222+
# Normal *
223+
for str in "" "-prod" "-dev"; do
224+
git remote set-url origin "zero$str:user/repo.git"
225+
run ../git-open
226+
assert_output "https://zero.com/user/repo"
227+
done
228+
# * with substitution
229+
for str in "" "-prod" "-dev"; do
230+
git remote set-url origin "subzero$str:user/repo.git"
231+
run ../git-open
232+
assert_output "https://subzero$str.zero/user/repo"
233+
done
234+
}
235+
236+
@test "sshconfig: host wildcard ? matches exactly one char" {
237+
create_ssh_sandbox
238+
# Normal ?
239+
for i in $(seq 1 3); do
240+
git remote set-url origin "one$i:user/repo.git"
241+
run ../git-open
242+
assert_output "https://one.com/user/repo"
243+
done
244+
# Refute invalid match on ?
245+
for str in "" "-test"; do
246+
git remote set-url origin "one:user/repo.git"
247+
run ../git-open
248+
refute_output "https://one$str.com/user/repo"
249+
done
250+
251+
# ? with substitution
252+
for i in $(seq 1 3); do
253+
git remote set-url origin "subone$i:user/repo.git"
254+
run ../git-open
255+
assert_output "https://subone$i.one/user/repo"
256+
done
257+
# Refute invalid match on ? with substitution
258+
for str in "" "-test"; do
259+
git remote set-url origin "subone$str:user/repo.git"
260+
run ../git-open
261+
refute_output "https://subone$str.one/user/repo"
262+
done
263+
# Refute invalid match on ? with substitution
264+
}
265+
266+
@test "sshconfig: overriding host rules" {
267+
create_ssh_sandbox
268+
git remote set-url origin "zero-override:user/repo.git"
269+
run ../git-open
270+
assert_output "https://override.zero.com/user/repo"
271+
}
165272

166273
##
167274
## Bitbucket
@@ -365,6 +472,9 @@ setup() {
365472
teardown() {
366473
cd ..
367474
rm -rf "$foldername"
475+
rm -rf "$ssh_config"
476+
refute [ -e "$ssh_config" ]
477+
unset ssh_config
368478
}
369479

370480
# helper to create a test git sandbox that won't dirty the real repo
@@ -390,3 +500,65 @@ function create_git_sandbox() {
390500
git add readme.txt
391501
git commit -m "add file" -q
392502
}
503+
504+
# helper to create test SSH config file
505+
function create_ssh_sandbox() {
506+
export ssh_config=$(mktemp)
507+
refute [ -z "$ssh_config" ]
508+
509+
# Populate ssh config with test data
510+
echo "$ssh_testdata" >$ssh_config
511+
assert [ -e "$ssh_config" ]
512+
}
513+
514+
# Test SSH config data
515+
ssh_testdata="
516+
# Autogenerated test sshconfig for paulirish/git-open BATS tests
517+
# It is safe to delete this file, a new one will be generated each test
518+
519+
Host basic
520+
HostName basic.com
521+
User git
522+
523+
Host nomatch
524+
User git
525+
526+
Host nouser
527+
HostName no.user
528+
529+
host malformed
530+
hOsTnAmE MaL.FoRmEd
531+
User other
532+
533+
# Multiple targets
534+
Host multi1 multi2 multi3
535+
HostName multi.com
536+
User git
537+
538+
Host sub1 sub2 sub3
539+
HostName %h.multi.com
540+
User git
541+
542+
# Wildcard * matching (zero or more characters)
543+
Host zero*
544+
HostName zero.com
545+
User git
546+
547+
Host subzero*
548+
HostName %h.zero
549+
User git
550+
551+
# Wildcard ? matching (exactly one character)
552+
Host one?
553+
HostName one.com
554+
User git
555+
556+
Host subone?
557+
HostName %h.one
558+
User git
559+
560+
# Overrides rule zero*
561+
Host zero-override
562+
HostName override.zero.com
563+
User git
564+
"

0 commit comments

Comments
 (0)