This commit is contained in:
a 2025-06-30 18:46:08 -05:00
parent 2e8dafded7
commit 46d7fbab1e
No known key found for this signature in database
GPG Key ID: 2F22877AA4DFDADB
10 changed files with 392 additions and 97 deletions

View File

@ -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

View File

@ -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}"

View File

@ -19,33 +19,57 @@ _activate_hook() {
return 1
}
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 $?
_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
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
usage:
repo get <repo-name>
EOF
;;
"open")
raw_url=$(git ls-remote --get-url | cut -d '@' -f2 | sed 's/:/\//1')
xdg-open "https://${raw_url}"
;;
esac
# 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"
# 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"

View File

@ -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

View File

@ -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

82
src/lib/git.sh Normal file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Parse a git URL into domain and path components
# Usage: parse_git_url <url> <perl_regex_command> <jq_command>
# 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 <perl_regex_command> <jq_command>
# 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 <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
}

View File

@ -1,47 +0,0 @@
# ((git|ssh|http(s)?)?|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)?(/)?
valid_url()
{
# matches https://<domain.tld>/<path>
if [[ "$1" =~ ^https?://.*\..*/.*$ ]]; then
echo '1'
return 0
fi
# matches <user>@<domain.*?>:<path>
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
}

49
src/open_command.sh Executable file
View File

@ -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}'

143
src/worktree_command.sh Executable file
View File

@ -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}'