Created
October 9, 2025 18:12
-
-
Save unixsurfer/9518a1e579a0661dcad4f0966714ee0d to your computer and use it in GitHub Desktop.
Script to generate a Git hosting URL (GitHub/GitLab) pointing to a specific file location in the repository with optional line numbers
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 | |
| # A script to generate a Git hosting URL (GitHub/GitLab) pointing to a specific file location in the repository with optional line numbers. | |
| # Usage: git-url.sh <file_path> [line_start[-line_end]] [--copy] | |
| # | |
| # Examples: | |
| # $ git-url.sh file.txt | |
| # https://github.com/user/repo/blob/main/file.txt | |
| # | |
| # $ git-url.sh file.txt 42 | |
| # https://github.com/user/repo/blob/main/file.txt#L42 | |
| # | |
| # $ git-url.sh file.txt 42-50 | |
| # https://gitlab.com/mygroup/myproject/-/blob/main/file.txt#L42-50 | |
| # | |
| # $ git-url.sh file.txt 42 --copy | |
| # https://github.com/user/repo/blob/main/file.txt#L42 | |
| # ✓ Copied to clipboard | |
| # Exit immediately if a command fails | |
| set -e | |
| # --- Helper Functions --- | |
| # URL encode a string (handles spaces and special characters) | |
| # Converts special characters to percent-encoded format for safe use in URLs | |
| # | |
| # Parameters: | |
| # $1 - The string to encode | |
| # | |
| # Returns: | |
| # URL-encoded string where special chars are converted to %HEX format | |
| # | |
| # Examples: | |
| # Input: my file.txt | |
| # Output: my%20file.txt | |
| # | |
| # Input: file#test.txt | |
| # Output: file%23test.txt | |
| # | |
| # Input: path/to/my document.md | |
| # Output: path/to/my%20document.md | |
| # | |
| # Note: Preserves alphanumeric chars, . ~ _ - and forward slashes | |
| url_encode() { | |
| local string="$1" | |
| local length="${#string}" | |
| local encoded="" | |
| # Loop through each character and encode if necessary | |
| for (( i = 0; i < length; i++ )); do | |
| local c="${string:i:1}" # Extract single character at position i | |
| case "$c" in | |
| [a-zA-Z0-9.~_-]) | |
| # Keep alphanumeric and URL-safe chars (a-z, A-Z, 0-9, . ~ _ -) as-is | |
| encoded+="$c" | |
| ;; | |
| /) | |
| # Keep forward slashes as-is (for directory separators in paths) | |
| encoded+="/" | |
| ;; | |
| *) # Default case: all other characters (spaces, #, %, etc.) | |
| # Convert char to %HEX format (e.g., space → %20, # → %23) | |
| # The '$c trick gets ASCII value, %02X formats as 2-digit hex | |
| printf -v encoded "%s%%%02X" "$encoded" "'$c" | |
| ;; | |
| esac | |
| done | |
| echo "$encoded" | |
| } | |
| # Convert remote URL to HTTPS base URL | |
| # Handles both SSH and HTTPS format Git remote URLs | |
| # | |
| # Parameters: | |
| # $1 - The Git remote URL (from 'git remote get-url origin') | |
| # | |
| # Returns: | |
| # HTTPS base URL without .git suffix | |
| # | |
| # Examples: | |
| # Input: [email protected]:mygroup/myproject.git | |
| # Output: https://gitlab.com/mygroup/myproject | |
| # | |
| # Input: https://gitlab.com/mygroup/myproject.git | |
| # Output: https://gitlab.com/mygroup/myproject | |
| # | |
| # Input: [email protected]:adyen/adyen-main.git | |
| # Output: https://gitlab.is.adyen.com/adyen/adyen-main | |
| parse_remote_url() { | |
| local remote_url="$1" | |
| local base_url="" | |
| # Handle SSH format: [email protected]:group/project.git | |
| if [[ "$remote_url" =~ ^git@([^:]+):(.+)$ ]]; then | |
| local host="${BASH_REMATCH[1]}" | |
| local path="${BASH_REMATCH[2]}" | |
| path="${path%.git}" # Remove .git suffix | |
| base_url="https://${host}/${path}" | |
| # Handle HTTPS format: https://gitlab.com/group/project.git | |
| elif [[ "$remote_url" =~ ^https?://(.+)$ ]]; then | |
| base_url="${remote_url%.git}" # Remove .git suffix | |
| else | |
| echo "Error: Unsupported remote URL format: $remote_url" >&2 | |
| exit 1 | |
| fi | |
| echo "$base_url" | |
| } | |
| # --- Pre-flight Checks --- | |
| # 1. Ensure we are inside a git repository | |
| if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then | |
| echo "Error: Not a git repository." >&2 | |
| exit 1 | |
| fi | |
| # 2. Ensure a filename argument was provided | |
| if [ -z "$1" ]; then | |
| echo "Usage: git-url.sh <file_path> [line_start[-line_end]] [--copy]" >&2 | |
| echo "" >&2 | |
| echo "Options:" >&2 | |
| echo " --copy Copy URL to clipboard (macOS only)" >&2 | |
| echo "" >&2 | |
| echo "Examples:" >&2 | |
| echo " git-url.sh file.txt" >&2 | |
| echo " git-url.sh file.txt 42" >&2 | |
| echo " git-url.sh file.txt 42-50" >&2 | |
| echo " git-url.sh file.txt 42 --copy" >&2 | |
| exit 1 | |
| fi | |
| file_arg="$1" | |
| line_arg="$2" | |
| copy_flag="$3" | |
| # Handle --copy flag in either position | |
| if [[ "$line_arg" == "--copy" ]]; then | |
| copy_flag="--copy" | |
| line_arg="" | |
| fi | |
| # 3. Ensure the file actually exists | |
| if [ ! -f "$file_arg" ]; then | |
| echo "Error: File '$file_arg' not found." >&2 | |
| exit 1 | |
| fi | |
| # --- Main Logic --- | |
| # 1. Get the repository's remote URL | |
| remote_url=$(git remote get-url origin 2>/dev/null) | |
| if [ -z "$remote_url" ]; then | |
| echo "Error: No remote 'origin' found." >&2 | |
| exit 1 | |
| fi | |
| # 2. Get the current branch name or commit SHA (for detached HEAD) | |
| branch=$(git branch --show-current) | |
| if [ -z "$branch" ]; then | |
| # Detached HEAD state - use commit SHA instead | |
| branch=$(git rev-parse HEAD) | |
| echo "Warning: Detached HEAD state, using commit SHA: $branch" >&2 | |
| fi | |
| # 3. Convert the remote URL into a clean base URL for the project | |
| base_url=$(parse_remote_url "$remote_url") | |
| # 4. Detect if it's GitHub or GitLab (GitHub doesn't use /-/ prefix) | |
| if [[ "$base_url" =~ github\.com ]]; then | |
| blob_path="/blob" | |
| else | |
| # GitLab and others use /-/blob/ | |
| blob_path="/-/blob" | |
| fi | |
| # 5. Get the file's path relative to the repository root | |
| current_dir_path="$(git rev-parse --show-prefix)" | |
| file_path="${current_dir_path}${file_arg}" | |
| # 6. URL encode the file path | |
| encoded_file_path=$(url_encode "$file_path") | |
| # 7. Build the URL with optional line numbers | |
| url="${base_url}${blob_path}/${branch}/${encoded_file_path}" | |
| # Add line number fragment if provided | |
| if [ -n "$line_arg" ]; then | |
| # Check if it's a range (e.g., "42-50") or single line (e.g., "42") | |
| if [[ "$line_arg" =~ ^([0-9]+)-([0-9]+)$ ]]; then | |
| # Line range | |
| url="${url}#L${BASH_REMATCH[1]}-${BASH_REMATCH[2]}" | |
| elif [[ "$line_arg" =~ ^[0-9]+$ ]]; then | |
| # Single line | |
| url="${url}#L${line_arg}" | |
| else | |
| echo "Error: Invalid line number format. Use '42' or '42-50'." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| # 7. Output the URL | |
| echo "$url" | |
| # 8. Optionally copy to clipboard (macOS only) | |
| if [[ "$copy_flag" == "--copy" ]]; then | |
| # Check if running on macOS | |
| if [[ "$OSTYPE" == "darwin"* ]]; then | |
| if command -v pbcopy > /dev/null 2>&1; then | |
| echo "$url" | pbcopy | |
| echo "✓ Copied to clipboard" >&2 | |
| else | |
| echo "Warning: pbcopy not found, cannot copy to clipboard" >&2 | |
| fi | |
| elif [[ "$OSTYPE" == "linux"* ]]; then | |
| # Linux: skip clipboard copy | |
| echo "Note: --copy flag is only supported on macOS" >&2 | |
| else | |
| echo "Warning: --copy flag is only supported on macOS" >&2 | |
| fi | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment