Last active
June 26, 2025 05:44
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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