mirror of
https://github.com/paulirish/git-open.git
synced 2026-09-10 07:26:15 -04:00
When using GitHub's SSH-over-HTTPS gateway (ssh.github.com), the remote URL is returned as ssh://git@ssh.github.com/..., causing git-open to generate invalid URLs like https://ssh.github.com/... This fix normalizes the domain back to github.com so that the generated browser URL is always valid.
316 lines
10 KiB
Bash
Executable file
316 lines
10 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
# Use git-sh-setup, similar to git-rebase
|
|
# https://www.kernel.org/pub/software/scm/git/docs/git-sh-setup.html
|
|
# https://github.com/git/git/blob/master/git-rebase.sh
|
|
# shellcheck disable=SC2034
|
|
OPTIONS_STUCKLONG=t
|
|
# shellcheck disable=SC2034
|
|
OPTIONS_KEEPDASHDASH=
|
|
# shellcheck disable=SC2034
|
|
OPTIONS_SPEC="\
|
|
git open [options]
|
|
git open [remote] [branch]
|
|
--
|
|
Opens the GitHub page for a repo/branch in your browser.
|
|
https://github.com/paulirish/git-open/
|
|
|
|
Available options are
|
|
c,commit! open current commit
|
|
i,issue! open issues page
|
|
s,suffix= append this suffix
|
|
f,file= append this file
|
|
p,print! just print the url
|
|
"
|
|
|
|
# https://github.com/koalaman/shellcheck/wiki/SC1090
|
|
# shellcheck source=/dev/null
|
|
SUBDIRECTORY_OK='Yes' . "$(git --exec-path)/git-sh-setup"
|
|
|
|
# Defaults
|
|
is_commit=0
|
|
is_issue=0
|
|
protocol="https"
|
|
print_only=0
|
|
suffix_flag=""
|
|
file_flag=""
|
|
|
|
while test $# != 0; do
|
|
case "$1" in
|
|
--commit) is_commit=1;;
|
|
--issue) is_issue=1;;
|
|
--suffix=*) suffix_flag="$1";;
|
|
--file=*) file_flag="$1";;
|
|
--print) print_only=1;;
|
|
--) shift; break ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# parse suffix from suffix=value
|
|
IFS='=' read -ra suffix_flag <<< "$suffix_flag"
|
|
function join_by { local IFS="$1"; shift; echo "$*"; }
|
|
suffix=$(join_by "=" "${suffix_flag[@]:1}")
|
|
|
|
# parse file from file=value
|
|
IFS='=' read -ra file_flag <<< "$file_flag"
|
|
file=$(join_by "=" "${file_flag[@]:1}")
|
|
|
|
# are we in a git repo?
|
|
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
|
echo "Not a git repository." 1>&2
|
|
exit 1
|
|
fi
|
|
|
|
# choose remote. priority to: provided argument, default in config, detected tracked remote, 'origin'
|
|
branch=${2:-$(git symbolic-ref -q --short HEAD)}
|
|
upstream_branch_full_name=$(git config "branch.$branch.merge")
|
|
upstream_branch=${upstream_branch_full_name#"refs/heads/"}
|
|
tracked_remote=$(git config "branch.$branch.remote")
|
|
default_remote=$(git config open.default.remote)
|
|
remote=${1:-$default_remote}
|
|
remote=${remote:-$tracked_remote}
|
|
remote=${remote:-"origin"}
|
|
|
|
giturl=$(git ls-remote --get-url "$remote")
|
|
|
|
if [[ -z "$giturl" ]]; then
|
|
echo "Git remote is not set for $remote" 1>&2
|
|
exit 1
|
|
fi
|
|
|
|
ssh_config=${ssh_config:-"$HOME/.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="$(echo "${ssh_array[0]}" | tr '[:lower:]' '[:upper:]')"
|
|
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
|
|
# Trim URL scheme and possible username
|
|
gitprotocol=${giturl%%://*}
|
|
uri=${giturl#*://}
|
|
uri=${uri#*@}
|
|
|
|
# Split on first '/ to get server name and path
|
|
domain=${uri%%/*}
|
|
urlpath=${uri#*/}
|
|
|
|
# Remove port number from non-http/https protocols (ie, ssh)
|
|
if [[ $gitprotocol != 'https' && $gitprotocol != 'http' ]]; then
|
|
domain=${domain%:*}
|
|
fi
|
|
else
|
|
# Trim possible username from SSH path
|
|
uri=${giturl##*@}
|
|
|
|
# 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 [[ -n "$domain_resolv" ]]; then
|
|
domain="$domain_resolv"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Normalize GitHub's SSH gateway hostname back to github.com for URL generation.
|
|
# ssh.github.com is used when connecting via SSH over port 443 (GitHub's SSH-over-HTTPS gateway),
|
|
# but the generated URL should still point to github.com.
|
|
domain=${domain//ssh\.github\.com/github.com}
|
|
|
|
# Trim "/" from beginning of URL; "/" and ".git" from end of URL
|
|
urlpath=${urlpath#/} urlpath=${urlpath%/} urlpath=${urlpath%.git}
|
|
|
|
# If the URL is provided as "http", preserve that
|
|
if [[ $gitprotocol == 'http' ]]; then
|
|
protocol='http'
|
|
fi
|
|
|
|
# Allow config options to replace the server or the protocol
|
|
openurl="$protocol://$domain"
|
|
|
|
function getConfig() {
|
|
config=$(git config --get-urlmatch "open.$1" "$openurl")
|
|
echo "${config:-${!1}}"
|
|
}
|
|
|
|
domain=$(getConfig "domain")
|
|
protocol=$(getConfig "protocol")
|
|
forge=$(getConfig "forge")
|
|
|
|
# Remote ref to open
|
|
remote_ref=${upstream_branch:-${branch:-$(git describe --tags --exact-match 2>/dev/null || git rev-parse HEAD)}}
|
|
|
|
# Split arguments on '/'
|
|
IFS='/' read -r -a pathargs <<<"$urlpath"
|
|
|
|
if (( is_issue )); then
|
|
# For issues, take the numbers and preprend 'issues/'
|
|
[[ $remote_ref =~ [0-9]+ ]]
|
|
providerBranchRef="/issues/${BASH_REMATCH[0]}"
|
|
else
|
|
# Make # and % characters url friendly
|
|
# github.com/paulirish/git-open/pull/24
|
|
remote_ref=${remote_ref//%/%25} remote_ref=${remote_ref//#/%23}
|
|
if [[ $forge == 'gitea' ]]; then
|
|
providerBranchRef="/src/branch/$remote_ref"
|
|
else
|
|
providerBranchRef="/tree/$remote_ref"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$domain" == 'bitbucket.org' ]]; then
|
|
providerBranchRef="/src/$remote_ref"
|
|
elif [[ "${#pathargs[@]}" -ge 3 && ${pathargs[${#pathargs[@]} - 3]} == 'scm' ]]; then
|
|
# Bitbucket server always has /scm/ as the third to last segment in the url path, e.g. /scm/ppp/test-repo.git
|
|
# Anything before the 'scm' is part of the server's root context
|
|
|
|
# If there are other context parts, add them, up to (but not including) the found 'scm'
|
|
pathPref=("${pathargs[*]:0:${#pathargs[@]} - 3}")
|
|
|
|
# Replace the 'scm' element, with 'projects'. Keep the first argument, the string 'repos', and finally the rest of the arguments.
|
|
# shellcheck disable=SC2206
|
|
pathargs=(${pathPref[@]} 'projects' ${pathargs[${#pathargs[@]} - 2]} 'repos' "${pathargs[@]:${#pathargs[@]} - 1}")
|
|
IFS='/' urlpath="${pathargs[*]}"
|
|
providerBranchRef="/browse?at=$remote_ref"
|
|
elif [[ "$domain" == *"bitbucket"* ]] && [[ $gitprotocol == 'ssh' ]]; then
|
|
# bitbucket repo cloned with ssh will not have /scm/ segment in url path
|
|
# e.g. /ppp/test-repo.git
|
|
|
|
# Build the repo url starting with 'projects' and then keep the first argument, the string 'repos', and finally the rest of the arguments.
|
|
# shellcheck disable=SC2206
|
|
pathargs=('projects' ${pathargs[${#pathargs[@]} - 2]} 'repos' "${pathargs[@]:${#pathargs[@]} - 1}")
|
|
IFS='/' urlpath="${pathargs[*]}"
|
|
providerBranchRef="/browse?at=$remote_ref"
|
|
elif [[ "${#pathargs[@]}" -ge '2' && ${pathargs[${#pathargs[@]} - 2]} == '_git' ]]; then
|
|
# Visual Studio Team Services and Team Foundation Server always have /_git/ as the second to last segment in the url path
|
|
if (( is_issue )); then
|
|
# Switch to workitems, provide work item id if specified
|
|
urlpath="${urlpath%%/_git/*}/_workitems"
|
|
# Handle case for the default repository url
|
|
urlpath="${urlpath#_git\/*}"
|
|
providerBranchRef="?id=${remote_ref//[^0-9]/}"
|
|
else
|
|
# Keep project and repository name, append branch selector.
|
|
providerBranchRef="?version=GB$remote_ref"
|
|
fi
|
|
elif [[ "$domain" =~ amazonaws\.com$ ]]; then
|
|
# AWS Code Commit
|
|
if (( is_issue )); then
|
|
echo "Issue feature does not supported on AWS Code Commit." 1>&2
|
|
exit 1
|
|
fi
|
|
|
|
# Take region name from domain.
|
|
region=${domain#*.}
|
|
region=${region%%.*}
|
|
|
|
# Replace domain.
|
|
# Ex. git-codecommit.us-east-1.amazonaws.com -> us-east-1.console.aws.amazon.com
|
|
domain="${region}.console.aws.amazon.com"
|
|
|
|
# Replace URL path.
|
|
# Ex. v1/repos/example -> codecommit/home?region=us-east-1#/repository/example/browse/
|
|
urlpath="codecommit/home?region=${region}#/repository/${urlpath##*/}/browse/"
|
|
|
|
# Replace branch ref.
|
|
# Ex. /tree/foobar -> foobar/--/
|
|
providerBranchRef="${providerBranchRef##*/}/--/"
|
|
elif [[ "$domain" =~ cnb\.cool$ ]]; then
|
|
# cnb.cool
|
|
# Replace URL path.
|
|
# Ex. repos/example -> repos/example/-
|
|
urlpath="$urlpath/-"
|
|
if [[ $remote_ref = "master" ]]; then
|
|
# repos/example/tree/master -> repos/example/-/tree/master
|
|
urlpath="$urlpath$providerBranchRef"
|
|
fi
|
|
fi
|
|
openurl="$protocol://$domain/$urlpath"
|
|
|
|
if (( is_commit )); then
|
|
sha=$(git rev-parse HEAD)
|
|
openurl="$openurl/commit/$sha"
|
|
elif [[ $remote_ref != "master" || "$file" ]]; then
|
|
# simplify URL for master
|
|
openurl="$openurl$providerBranchRef"
|
|
fi
|
|
|
|
if [ "$file" ]; then
|
|
absfile=$(git ls-tree --full-name --name-only "$branch" "$file")
|
|
if [[ -z "$absfile" ]]; then
|
|
echo "File $file is not in repository" 1>&2
|
|
exit 1
|
|
fi
|
|
openurl="$openurl/$absfile"
|
|
fi
|
|
|
|
if [ "$suffix" ]; then
|
|
openurl="$openurl/$suffix"
|
|
fi
|
|
|
|
# get current open browser command
|
|
case $( uname -s ) in
|
|
Darwin) open='open';;
|
|
MINGW*) open='start';;
|
|
MSYS*) open='start';;
|
|
CYGWIN*) open='cygstart';;
|
|
*) # Try to detect WSL (Windows Subsystem for Linux)
|
|
if uname -r | grep -q -i Microsoft; then
|
|
open='/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -NoProfile Start'
|
|
else
|
|
open='xdg-open'
|
|
fi;;
|
|
esac
|
|
|
|
if (( print_only )); then
|
|
BROWSER="echo"
|
|
fi
|
|
|
|
# open it in a browser
|
|
${BROWSER:-$open} "$openurl"
|