From 46d7fbab1e9b9aa5b46e97a82cfbfb08964b30b9 Mon Sep 17 00:00:00 2001 From: a Date: Mon, 30 Jun 2025 18:46:08 -0500 Subject: [PATCH] noot --- Makefile | 8 +-- libraries.yml | 5 ++ repotool.zsh | 82 +++++++++++++-------- src/bashly.yml | 39 ++++++++++ src/get_command.sh | 34 ++++----- src/lib/git.sh | 82 +++++++++++++++++++++ src/lib/repotool/stdlib.sh | 47 ------------ {lib => src/lib}/stdlib.sh | 0 src/open_command.sh | 49 +++++++++++++ src/worktree_command.sh | 143 +++++++++++++++++++++++++++++++++++++ 10 files changed, 392 insertions(+), 97 deletions(-) create mode 100644 src/lib/git.sh delete mode 100644 src/lib/repotool/stdlib.sh rename {lib => src/lib}/stdlib.sh (100%) create mode 100755 src/open_command.sh create mode 100755 src/worktree_command.sh diff --git a/Makefile b/Makefile index 8684fe7..b3cf1cc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: all install -SOURCES_LIBS:=$(shell find lib -type f) +SOURCES_LIBS:=$(shell find src/lib -type f) SOURCES_SRC:=$(shell find src -type f ) REPOTOOL_PATH ?= ${HOME}/repo @@ -15,8 +15,8 @@ src/lib/repotool/stdlib.sh: $(SOURCES_LIBS) bashly add --source . stdlib -f dist/repotool: $(SOURCES_SRC) - mkdir -p dist - bashly generate - mv repotool dist + @mkdir -p dist + @bashly generate + @mv repotool dist diff --git a/libraries.yml b/libraries.yml index 8408d5c..89ed18d 100644 --- a/libraries.yml +++ b/libraries.yml @@ -3,3 +3,8 @@ stdlib: files: - source: "lib/stdlib.sh" target: "%{user_lib_dir}/repotool/stdlib.%{user_ext}" +git: + help: git for repotool + files: + - source: "lib/git.sh" + target: "%{user_lib_dir}/repotool/git.%{user_ext}" diff --git a/repotool.zsh b/repotool.zsh index ed6153a..be97aef 100755 --- a/repotool.zsh +++ b/repotool.zsh @@ -19,33 +19,57 @@ _activate_hook() { return 1 } +_parse_json() { + local response="$1" + local key="$2" + echo "$response" | jq -r ".$key" +} + +_handle_response() { + local response="$1" + + # Validate JSON + if ! echo "$response" | jq . >/dev/null 2>&1; then + # Not valid JSON, write to stderr + echo "$response" >&2 + return + fi + + # Check if response has an echo field + local echo_msg=$(echo "$response" | jq -r '.echo // empty') + if [[ -n "$echo_msg" ]]; then + echo "$echo_msg" + fi + + # Get hook name from response + local hook_name=$(echo "$response" | jq -r '.hook // empty') + + # Handle before hook if hook name is provided + if [[ -n "$hook_name" ]]; then + _activate_hook "before_${hook_name}_cd" "$response" + fi + + # Check if response has a cd field + local cd_path=$(echo "$response" | jq -r '.cd // empty') + if [[ -n "$cd_path" ]]; then + cd "$cd_path" + fi + + # Handle after hook if hook name is provided + if [[ -n "$hook_name" ]]; then + _activate_hook "after_${hook_name}_cd" "$response" + fi +} + TOOL_BIN="$REPOTOOL_PATH/.bin/repotool" -case "$1" in - get | g) - shift; - response=$($TOOL_BIN get $@) - if [[ $? != 0 ]] then; - echo "failed to get repo with args $@" - return $? - fi - declare -A obj - for item in ${(z)response}; do - parts=(${(s[=])item}) - # NOTE: zsh is 1 indexed arrays - obj[${parts[1]}]=${parts[2]} - done - _activate_hook "before_cd" $response - cd ${obj[dir]} - _activate_hook "after_cd" $response - ;; - 'help' | "-h"| "-help" | "--help") - echo < -EOF - ;; - "open") - raw_url=$(git ls-remote --get-url | cut -d '@' -f2 | sed 's/:/\//1') - xdg-open "https://${raw_url}" - ;; -esac + +# Pass all arguments to repotool and handle response +response=$($TOOL_BIN "$@") +exit_code=$? + +if [[ $exit_code != 0 ]]; then + echo "Command failed with exit code $exit_code" >&2 + return $exit_code +fi + +_handle_response "$response" \ No newline at end of file diff --git a/src/bashly.yml b/src/bashly.yml index 2aebccd..0b6772b 100644 --- a/src/bashly.yml +++ b/src/bashly.yml @@ -19,6 +19,8 @@ commands: command: ["git"] perl: command: ["perl"] + jq: + command: ["jq"] args: - name: repo required: true @@ -44,3 +46,40 @@ commands: allowed: ["ssh", "https", "http"] examples: - repo get tuxpa.in/a/repotool +- name: worktree + alias: [w, wt] + help: get worktree path for current repo + dependencies: + git: + command: ["git"] + perl: + command: ["perl"] + jq: + command: ["jq"] + args: + - name: name + required: false + help: Name of the worktree + flags: + - long: --list + short: -l + help: List existing worktrees for this repo + - long: --root + short: -r + help: Return the root directory of the original repo + examples: + - repo worktree feature-branch + - repo worktree -l + - repo worktree -r +- name: open + alias: o + help: open the current repository in web browser + dependencies: + git: + command: ["git"] + perl: + command: ["perl"] + jq: + command: ["jq"] + examples: + - repo open diff --git a/src/get_command.sh b/src/get_command.sh index b866c7e..bb37137 100755 --- a/src/get_command.sh +++ b/src/get_command.sh @@ -1,4 +1,4 @@ - +#!/bin/bash linspect local resp @@ -21,22 +21,14 @@ local ssh_user; local http_user; local http_pass; -local domain -local path -#now extract the args we need -# this is where we use perl -if [[ $resp == 1 ]]; then - # TODO: properly extract user and password, if exists. - output=($(echo ${args[repo]} | ${regex} '/^https?:\/\/(?:.*?:)?(.*\..*?)\/(.*?)(\.git)?$/ && print "$1 $2" ' -)) - domain=${output[0]} - path=${output[1]} -fi +# Parse the URL to get domain and path +parsed_json=$(parse_git_url "${args[repo]}" "$regex" "${deps[jq]}") +domain=$(echo "$parsed_json" | ${deps[jq]} -r '.domain') +path=$(echo "$parsed_json" | ${deps[jq]} -r '.path') -if [[ $resp == 2 ]]; then - # TODO: properly extract ssh user, if exists. - output=($(echo ${args[repo]} | ${regex} '/^(?:.*?@)?(.*\..*?)(?::|\/)(.*?)(\.git)?$/ && print "$1 $2" ' -)) - domain=${output[0]} - path=${output[1]} +if [[ -z "$domain" ]] || [[ -z "$path" ]]; then + echo "Failed to parse repository URL: ${args[repo]}" + exit 1 fi if [[ ! -z "${args[--ssh-user]}" && -z "$ssh_user" ]]; then @@ -109,6 +101,14 @@ if [[ ! -d .git ]]; then fi fi -echo dir=$target_dir domain=$domain path=$path repo_url=$repo_url cloned=$cloned +# Output in JSON format +${deps[jq]} -n \ + --arg cd "$target_dir" \ + --arg domain "$domain" \ + --arg path "$path" \ + --arg repo_url "$repo_url" \ + --arg cloned "$cloned" \ + --arg hook "get" \ + '{cd: $cd, domain: $domain, path: $path, repo_url: $repo_url, cloned: $cloned, hook: $hook}' exit 0 diff --git a/src/lib/git.sh b/src/lib/git.sh new file mode 100644 index 0000000..905fd9d --- /dev/null +++ b/src/lib/git.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +# Parse a git URL into domain and path components +# Usage: parse_git_url +# Returns: JSON with domain and path fields +parse_git_url() { + local url="$1" + local regex="$2" + local jq="$3" + + local domain="" + local path="" + local output + + # Check if it's an HTTP(S) URL + if [[ "$url" =~ ^https?:// ]]; then + output=($(echo "$url" | $regex '/^https?:\/\/(?:.*?:)?(.*\..*?)\/(.*?)(\.git)?$/ && print "$1 $2" ' -)) + if [[ ${#output[@]} -eq 2 ]]; then + domain=${output[0]} + path=${output[1]} + fi + # Check if it's an SSH URL + elif [[ "$url" =~ ^[^:]+@[^:]+: ]] || [[ "$url" =~ ^ssh:// ]]; then + output=($(echo "$url" | $regex '/^(?:.*?@)?(.*\..*?)(?::|\/)(.*?)(\.git)?$/ && print "$1 $2" ' -)) + if [[ ${#output[@]} -eq 2 ]]; then + domain=${output[0]} + path=${output[1]} + fi + # Check if it's a bare domain path (e.g., gfx.cafe/oku/trade) + else + output=($(echo "$url" | $regex '/^(.*\..*?)\/(.*?)(\.git)?$/ && print "$1 $2" ' -)) + if [[ ${#output[@]} -eq 2 ]]; then + domain=${output[0]} + path=${output[1]} + fi + fi + + # Return JSON + $jq -n --arg domain "$domain" --arg path "$path" '{domain: $domain, path: $path}' +} + +# Get domain and path from current git repository's origin +# Usage: parse_git_origin +# Returns: JSON with domain and path fields +parse_git_origin() { + local regex="$1" + local jq="$2" + + # Get origin URL + local origin_url + origin_url=$(git config --get remote.origin.url) + + if [[ -z "$origin_url" ]]; then + $jq -n '{domain: "", path: ""}' + return 1 + fi + + # Parse the URL + parse_git_url "$origin_url" "$regex" "$jq" +} + +# Validate if a URL is a valid git repository URL +# Usage: valid_url +# Returns: 1 for HTTP(S), 2 for SSH, 3 for bare domain paths, -1 for invalid +valid_url() { + local url="$1" + + if [[ "$url" =~ ^https?:// ]]; then + echo "1" + return 0 + elif [[ "$url" =~ ^[^:]+@[^:]+: ]] || [[ "$url" =~ ^ssh:// ]]; then + echo "2" + return 0 + elif [[ "$url" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]+/[^/]+ ]]; then + # Bare domain path like gfx.cafe/oku/trade + echo "3" + return 0 + else + echo "-1" + return 1 + fi +} \ No newline at end of file diff --git a/src/lib/repotool/stdlib.sh b/src/lib/repotool/stdlib.sh deleted file mode 100644 index 05321d2..0000000 --- a/src/lib/repotool/stdlib.sh +++ /dev/null @@ -1,47 +0,0 @@ - - -# ((git|ssh|http(s)?)?|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)?(/)? -valid_url() -{ - # matches https:/// - if [[ "$1" =~ ^https?://.*\..*/.*$ ]]; then - echo '1' - return 0 - fi - # matches @: - if [[ "$1" =~ ^(.*?)(:|/)(.+)(\/.+)*$ ]]; then - echo '2' - return 0 - fi - echo '-1' - return 0 -} - -lcat() -{ - if [[ -z "$DEBUG_LOG" || "$DEBUG_LOG" == 0 ]]; then - return 0 - fi - cat $@ >&2 -} - -lecho() -{ - if [[ -z "$DEBUG_LOG" || "$DEBUG_LOG" == 0 ]]; then - return 0 - fi - - echo $@ >&2 -} - -linspect() -{ - if [[ -z "$DEBUG_LOG" || "$DEBUG_LOG" == 0 ]]; then - return 0 - fi - - inspect_args>&2 -} - - - diff --git a/lib/stdlib.sh b/src/lib/stdlib.sh similarity index 100% rename from lib/stdlib.sh rename to src/lib/stdlib.sh diff --git a/src/open_command.sh b/src/open_command.sh new file mode 100755 index 0000000..b77cd08 --- /dev/null +++ b/src/open_command.sh @@ -0,0 +1,49 @@ +linspect + +# Check if we're in a git repository +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "Error: Not in a git repository" >&2 + exit 1 +fi + +# Get the remote URL +raw_url=$(git ls-remote --get-url) +if [[ -z "$raw_url" ]]; then + echo "Error: No remote URL found" >&2 + exit 1 +fi + +# Parse the URL to get domain and path +local regex="${deps[perl]} -n -l -e" +parsed_json=$(parse_git_url "$raw_url" "$regex" "${deps[jq]}") +domain=$(echo "$parsed_json" | ${deps[jq]} -r '.domain') +path=$(echo "$parsed_json" | ${deps[jq]} -r '.path') + +if [[ -z "$domain" ]] || [[ -z "$path" ]]; then + echo "Error: Unable to parse repository URL: $raw_url" >&2 + exit 1 +fi + +# Construct the HTTPS URL +https_url="https://${domain}/${path}" + +# Detect the platform and open the URL +if command -v xdg-open &> /dev/null; then + # Linux + xdg-open "$https_url" 2>/dev/null +elif command -v open &> /dev/null; then + # macOS + open "$https_url" 2>/dev/null +elif command -v start &> /dev/null; then + # Windows + start "$https_url" 2>/dev/null +else + echo "Error: Unable to detect platform open command" >&2 + exit 1 +fi + +# Return JSON with echo message +${deps[jq]} -n \ + --arg echo "Opening $https_url in browser..." \ + --arg hook "open" \ + '{echo: $echo, hook: $hook}' diff --git a/src/worktree_command.sh b/src/worktree_command.sh new file mode 100755 index 0000000..0f5eaa1 --- /dev/null +++ b/src/worktree_command.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +# Get the current directory (should be inside a git repo) +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "Error: Not in a git repository" >&2 + exit 1 +fi + +# Get the repository root +repo_root=$(git rev-parse --show-toplevel) + +# Handle --root flag +if [[ "${args[--root]}" == "1" ]]; then + # Check if we're in a worktree + git_common_dir=$(git rev-parse --git-common-dir 2>/dev/null) + if [[ "$git_common_dir" != ".git" && -d "$git_common_dir" ]]; then + # We're in a worktree, get the main repo path + main_repo=$(dirname "$git_common_dir") + ${deps[jq]} -n --arg cd "$main_repo" --arg hook "worktree" '{cd: $cd, hook: $hook}' + else + # We're in the main repo already + ${deps[jq]} -n --arg cd "$repo_root" --arg hook "worktree" '{cd: $cd, hook: $hook}' + fi + exit 0 +fi + +# Parse the origin URL to get domain and path +local regex="${deps[perl]} -n -l -e" +parsed_json=$(parse_git_origin "$regex" "${deps[jq]}") +domain=$(echo "$parsed_json" | ${deps[jq]} -r '.domain') +path=$(echo "$parsed_json" | ${deps[jq]} -r '.path') + +if [[ -z "$domain" ]] || [[ -z "$path" ]]; then + echo "Error: Unable to parse repository origin URL" >&2 + exit 1 +fi + +# Calculate the worktree base directory +worktree_base="$REPOTOOL_PATH/worktree/$domain/$path" + +# Check if name argument is "list" or "ls" +if [[ "${args[name]}" == "list" ]] || [[ "${args[name]}" == "ls" ]]; then + args[--list]="1" +fi + +# Handle --list flag +if [[ "${args[--list]}" == "1" ]]; then + echo "Worktrees for $domain/$path:" >&2 + + # Get all worktrees from git using porcelain format + local found_any=false + local wt_path="" + local is_prunable=false + + while IFS= read -r line; do + # Parse git worktree list --porcelain output + if [[ "$line" =~ ^worktree[[:space:]](.+)$ ]]; then + # Process previous worktree if any + if [[ -n "$wt_path" ]] && [[ "$wt_path" == "$worktree_base/"* ]]; then + local wt_name=$(basename "$wt_path") + if [[ -d "$wt_path" ]]; then + echo " - $wt_name" >&2 + else + if [[ "$is_prunable" == "true" ]]; then + echo " - $wt_name (missing - prunable)" >&2 + else + echo " - $wt_name (missing)" >&2 + fi + fi + found_any=true + fi + + # Start new worktree + wt_path="${BASH_REMATCH[1]}" + is_prunable=false + elif [[ "$line" == "prunable gitdir file points to non-existent location" ]]; then + is_prunable=true + fi + done < <(git worktree list --porcelain) + + # Process the last worktree + if [[ -n "$wt_path" ]] && [[ "$wt_path" == "$worktree_base/"* ]]; then + local wt_name=$(basename "$wt_path") + if [[ -d "$wt_path" ]]; then + echo " - $wt_name" >&2 + else + if [[ "$is_prunable" == "true" ]]; then + echo " - $wt_name (missing - prunable)" >&2 + else + echo " - $wt_name (missing)" >&2 + fi + fi + found_any=true + fi + + if [[ "$found_any" == "false" ]]; then + echo " No worktrees found" >&2 + fi + + # Return hook field only (no cd field) so we don't change dirs + ${deps[jq]} -n --arg hook "worktree.list" '{hook: $hook}' + exit 0 +fi + +# Get the worktree name from arguments +worktree_name="${args[name]}" + +# Check if name is provided (required for normal operation) +if [[ -z "$worktree_name" ]]; then + echo "Error: Missing required argument: name" >&2 + echo "Run 'repo worktree --help' for usage information" >&2 + exit 1 +fi + +# Construct the worktree path +worktree_path="$worktree_base/$worktree_name" + +# Check if a prunable worktree exists at this path +if git worktree list | grep -q "$worktree_path.*prunable"; then + echo "Found prunable worktree at $worktree_path, cleaning up..." >&2 + git worktree prune +fi + +# Check if worktree already exists +created="false" +if ! git worktree list | grep -q "$worktree_path"; then + # Create parent directories if they don't exist + mkdir -p "$(dirname "$worktree_path")" + + # Create the worktree + git worktree add "$worktree_path" -b "$worktree_name" 2>/dev/null || git worktree add "$worktree_path" "$worktree_name" 2>/dev/null || git worktree add "$worktree_path" + created="true" +fi + +# Output in JSON format +${deps[jq]} -n \ + --arg cd "$worktree_path" \ + --arg domain "$domain" \ + --arg path "$path" \ + --arg worktree_name "$worktree_name" \ + --arg created "$created" \ + --arg hook "worktree" \ + '{cd: $cd, domain: $domain, path: $path, worktree_name: $worktree_name, created: $created, hook: $hook}'