mirror of
https://github.com/paulirish/git-open.git
synced 2026-09-10 07:26:15 -04:00
This is guaranteed to be always at this location and is always available if Interopt is enabled. With this it is no longer required to have powershell.exe in PATH which significantly slows down tab completion due to the very slow fs access times on WSL2. Before git-open would silently fail if powershell.exe was not available.
278 lines
8.7 KiB
Bash
Executable file
278 lines
8.7 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
|
|
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=""
|
|
|
|
while test $# != 0; do
|
|
case "$1" in
|
|
--commit) is_commit=1;;
|
|
--issue) is_issue=1;;
|
|
--suffix=*) suffix_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}")
|
|
|
|
# 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
|
|
|
|
# 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 [[ "${#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##*/}/--/"
|
|
fi
|
|
|
|
openurl="$protocol://$domain/$urlpath"
|
|
|
|
if (( is_commit )); then
|
|
sha=$(git rev-parse HEAD)
|
|
openurl="$openurl/commit/$sha"
|
|
elif [[ $remote_ref != "master" ]]; then
|
|
# simplify URL for master
|
|
openurl="$openurl$providerBranchRef"
|
|
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"
|