Skip to content

Instantly share code, notes, and snippets.

@anhkhoakz
Last active June 26, 2025 05:44
Show Gist options
  • Select an option

  • Save anhkhoakz/0c8591090eaaed7594a9323f90ece64e to your computer and use it in GitHub Desktop.

Select an option

Save anhkhoakz/0c8591090eaaed7594a9323f90ece64e to your computer and use it in GitHub Desktop.
Open or copy the web URL for a file, directory, branch, or commit in a git repository
#!/usr/bin/env bash
# git-browse.sh - Open or copy the web URL for a file, directory, branch,
# or commit in a git repository.
#
# Usage: git-browse.sh [--dry-run] [--copy] [--path RELPATH] [target]
set -e
SCRIPT_NAME="$(basename "$0")"
DEFAULT_BRANCH_NAME="main"
readonly SCRIPT_NAME
readonly SUPPORTED_GIT_HOSTS=(
"github.com"
"gitlab.com"
"bitbucket.org"
"git.sr.ht"
"codeberg.org"
)
#######################################
# Print error message to STDERR.
# Globals:
# SCRIPT_NAME
# Arguments:
# $1 - Error message
# Outputs:
# Writes error message to stderr
#######################################
print_error() {
printf '%s: %s\n' "$SCRIPT_NAME" "$1" >&2
}
#######################################
# Find the root of the git repository.
# Outputs:
# Writes absolute path to repo root to stdout
# Returns:
# 0 if found, 1 if not found
#######################################
find_repo_root() {
git rev-parse --show-toplevel 2>/dev/null || {
print_error ".git directory not found"
return 1
}
}
#######################################
# Get the remote URL from git config.
# Arguments:
# $1 - Repository root
# Outputs:
# Writes remote.origin.url to stdout
# Returns:
# 0 on success, 1 on error
#######################################
get_remote_url() {
git -C "$1" config --get remote.origin.url || {
print_error "Failed to get remote.origin.url"
return 1
}
}
#######################################
# Get the default branch from git config.
# Globals:
# DEFAULT_BRANCH_NAME
# Arguments:
# $1 - Repository root
# Outputs:
# Writes default branch name to stdout
# Returns:
# 0 on success
#######################################
get_default_branch() {
git -C "$1" symbolic-ref --short HEAD 2>/dev/null || \
echo "$DEFAULT_BRANCH_NAME"
}
#######################################
# Convert SSH/HTTPS git URL to web URL.
# Arguments:
# $1 - Git remote URL
# Outputs:
# Writes web URL to stdout
# Returns:
# 0 on success, 1 if unsupported host
#######################################
git_url_to_web_url() {
url="$1"
for host in "${SUPPORTED_GIT_HOSTS[@]}"; do
if [[ "$url" =~ ${host}[:/](.+)/(.+)(\.git)?$ ]]; then
user="${BASH_REMATCH[1]}"
repo="${BASH_REMATCH[2]}"
echo "https://${host}/$user/${repo%.git}"
return 0
fi
done
print_error "Unsupported git host: $url"
return 1
}
#######################################
# Detect platform and set PLATFORM global variable.
# Globals:
# PLATFORM
#######################################
platform_detect() {
uname_out="$(uname -s)"
if [[ "$uname_out" == "Darwin" ]]; then
PLATFORM="darwin"
elif [[ "$uname_out" == CYGWIN* ]]; then
PLATFORM="cygwin"
elif [[ -n "${TERMUX_VERSION-}" ]]; then
PLATFORM="termux"
elif [[ "${XDG_SESSION_TYPE:-}" == 'wayland' ]]; then
PLATFORM="wayland"
elif command -v xclip >/dev/null 2>&1; then
PLATFORM="xclip"
elif command -v xsel >/dev/null 2>&1; then
PLATFORM="xsel"
else
PLATFORM="none"
fi
}
#######################################
# Copy a string to the clipboard (cross-platform).
# Globals:
# PLATFORM
# Arguments:
# $1 - String to copy
# Returns:
# 0 on success, 1 on error
#######################################
copy_to_clipboard() {
platform_detect
case "$PLATFORM" in
darwin)
if ! command -v pbcopy >/dev/null 2>&1; then
print_error "pbcopy not found"
return 1
fi
printf '%s' "$1" | pbcopy
;;
cygwin)
printf '%s' "$1" | tee > /dev/clipboard
;;
termux)
if ! command -v termux-clipboard-set >/dev/null 2>&1; then
print_error "termux-clipboard-set not found"
return 1
fi
printf '%s' "$1" | termux-clipboard-set
;;
wayland)
if ! command -v wl-copy >/dev/null 2>&1; then
print_error "wl-copy not found"
return 1
fi
printf '%s' "$1" | wl-copy
;;
xclip)
printf '%s' "$1" | xclip -selection clipboard -in
;;
xsel)
printf '%s' "$1" | xsel --clipboard --input
;;
*)
print_error "No clipboard utility found for platform: $PLATFORM"
return 1
;;
esac
}
#######################################
# Open a URL in the default browser (cross-platform).
# Globals:
# PLATFORM
# Arguments:
# $1 - URL to open
# Returns:
# 0 on success, 1 on error
#######################################
open_url() {
platform_detect
case "$PLATFORM" in
darwin)
if command -v open >/dev/null 2>&1; then
open "$1"
else
print_error "'open' not found"
fi
;;
cygwin)
if command -v cygstart >/dev/null 2>&1; then
cygstart "$1"
else
print_error "'cygstart' not found"
fi
;;
termux)
if command -v termux-open >/dev/null 2>&1; then
termux-open "$1"
else
print_error "'termux-open' not found"
fi
;;
wayland|xclip|xsel|other)
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$1"
else
print_error "'xdg-open' not found"
fi
;;
*)
print_error "No open command found for platform: $PLATFORM"
return 1
;;
esac
}
#######################################
# Print usage information.
# Globals:
# SCRIPT_NAME
# Outputs:
# Writes usage to stdout
#######################################
print_usage() {
cat <<EOF
Usage: $SCRIPT_NAME [--dry-run] [--copy] [--path RELPATH] [target]
EOF
}
#######################################
# Validate input arguments.
# Arguments:
# All script arguments
# Outputs:
# Error message to stderr if invalid
# Returns:
# 0 if valid, 1 if invalid
#######################################
validate_args() {
if [[ $# -gt 4 ]]; then
print_error "Too many arguments"
print_usage
return 1
fi
return 0
}
# Build the final web URL for the target
# Arguments: $1 - Repository root, $2 - Web URL, $3 - Default branch,
# $4 - Relative path, $5 - Target
# Returns the final URL or prints an error if the target is not found
build_final_url() {
repo_root="$1"; web_url="$2"; default_branch="$3"; relpath="$4";
target="$5"
if [[ -n "$relpath" ]]; then
path_prefix="$relpath/"
else
path_prefix=""
fi
if [[ -z "$target" ]]; then
echo "$web_url"
return 0
fi
if [[ -d "$repo_root/$path_prefix$target" ]]; then
echo "$web_url/tree/$default_branch/$path_prefix$target"
return 0
fi
if [[ -f "$repo_root/$path_prefix$target" ]]; then
echo "$web_url/blob/$default_branch/$path_prefix$target"
return 0
fi
echo "$web_url/commit/$target"
return 0
}
main() {
dry_run=0; copy_flag=0; relpath=""; target=""
validate_args "$@" || exit 1
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run|-d)
dry_run=1
;;
--copy|-c)
copy_flag=1
;;
--path)
relpath="$2"
shift
;;
-h|--help)
print_usage
exit 0
;;
*)
target="$1"
;;
esac
shift
done
repo_root=$(find_repo_root) || exit 1
remote_url=$(get_remote_url "$repo_root") || exit 1
web_url=$(git_url_to_web_url "$remote_url") || exit 1
default_branch=$(get_default_branch "$repo_root")
final_url=$(build_final_url "$repo_root" "$web_url" "$default_branch" \
"$relpath" "$target")
echo "$final_url"
if (( copy_flag == 1 )); then
copy_to_clipboard "$final_url" || exit 1
fi
if (( dry_run == 0 )); then
open_url "$final_url"
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment