306 lines
11 KiB
Bash
306 lines
11 KiB
Bash
#!/usr/bin/env bash
|
|
# Common utilities for gitlink-cli workflow scripts
|
|
|
|
set -euo pipefail
|
|
# Trap SIGPIPE to prevent premature exit when piping through head/truncate
|
|
trap '' PIPE
|
|
|
|
# Ensure jq is available (WinGet installs to non-default PATH on Windows)
|
|
if ! command -v jq &>/dev/null; then
|
|
for d in "$LOCALAPPDATA/Microsoft/WinGet/Links" "$HOME/AppData/Local/Microsoft/WinGet/Links"; do
|
|
[[ -d "$d" ]] && export PATH="$d:$PATH"
|
|
done
|
|
fi
|
|
|
|
# Ensure CLAUDE_CODE_GIT_BASH_PATH is set for Windows (needed by claude CLI)
|
|
if [[ -z "${CLAUDE_CODE_GIT_BASH_PATH:-}" ]] && command -v cygpath &>/dev/null; then
|
|
export CLAUDE_CODE_GIT_BASH_PATH="$(cygpath -w "$(which bash)")"
|
|
fi
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m'
|
|
|
|
# ── Logging ──────────────────────────────────────────────────────────
|
|
log_step() { echo -e "${BLUE}[STEP]${NC} $*"; }
|
|
log_ok() { echo -e "${GREEN}[ OK]${NC} $*"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
log_err() { echo -e "${RED}[ ERR]${NC} $*" >&2; }
|
|
log_info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
|
log_title(){ echo -e "\n${BOLD}══════ $* ══════${NC}\n"; }
|
|
|
|
# ── Auth Check ───────────────────────────────────────────────────────
|
|
check_auth() {
|
|
# Check env var first, then try CLI auth status
|
|
if [[ -n "${GITLINK_TOKEN:-}" ]]; then
|
|
log_ok "GITLINK_TOKEN is set"
|
|
return 0
|
|
fi
|
|
local status
|
|
status=$(gitlink-cli auth status 2>&1)
|
|
if echo "$status" | grep -qi "logged in\|✓"; then
|
|
log_ok "Authenticated: $(echo "$status" | sed -n 's/.*as //p' | tr -d '[:space:]')"
|
|
return 0
|
|
fi
|
|
log_err "Not authenticated. Please login first:"
|
|
log_info " gitlink-cli auth login"
|
|
log_info " export GITLINK_TOKEN=\"your-private-token\""
|
|
exit 1
|
|
}
|
|
|
|
# ── JSON Helpers ─────────────────────────────────────────────────────
|
|
# Extract a field from CLI JSON output (Envelope: {ok, data, ...})
|
|
json_ok() {
|
|
echo "$1" | jq -r '.ok // false' 2>/dev/null
|
|
}
|
|
|
|
json_data() {
|
|
echo "$1" | jq -r '.data' 2>/dev/null
|
|
}
|
|
|
|
json_get() {
|
|
echo "$1" | jq -r "$2" 2>/dev/null
|
|
}
|
|
|
|
json_error() {
|
|
echo "$1" | jq -r '.error.message // "unknown error"' 2>/dev/null
|
|
}
|
|
|
|
# ── CLI Wrapper ──────────────────────────────────────────────────────
|
|
GL="gitlink-cli"
|
|
|
|
gl_run() {
|
|
local output
|
|
# Always use JSON format for scripting
|
|
output=$("$GL" "$@" --format json 2>&1) || true
|
|
echo "$output"
|
|
}
|
|
|
|
gl_check() {
|
|
local output
|
|
output=$(gl_run "$@")
|
|
# Check if output is valid JSON (use here-string to avoid SIGPIPE)
|
|
if ! jq empty <<< "$output" 2>/dev/null; then
|
|
log_err "Command failed (non-JSON response): $GL $*"
|
|
log_err "$output"
|
|
return 1
|
|
fi
|
|
if [[ "$(json_ok "$output")" != "true" ]]; then
|
|
log_err "Command failed: $GL $*"
|
|
log_err "$(json_error "$output")"
|
|
return 1
|
|
fi
|
|
echo "$output"
|
|
}
|
|
|
|
# ── Owner/Repo Detection ────────────────────────────────────────────
|
|
detect_owner_repo() {
|
|
local remote_url
|
|
remote_url=$(git remote get-url origin 2>/dev/null || echo "")
|
|
if [[ -z "$remote_url" ]]; then
|
|
log_err "No git remote 'origin' found. Use --owner and --repo flags."
|
|
exit 1
|
|
fi
|
|
# Parse gitlink URL patterns
|
|
# https://gitlink.org.cn/owner/repo.git or git@gitlink.org.cn:owner/repo.git
|
|
if [[ "$remote_url" =~ gitlink\.org\.cn[:/]([^/]+)/([^/.]+) ]]; then
|
|
DETECTED_OWNER="${BASH_REMATCH[1]}"
|
|
DETECTED_REPO="${BASH_REMATCH[2]}"
|
|
else
|
|
log_err "Cannot parse owner/repo from remote: $remote_url"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_owner_repo() {
|
|
if [[ -z "${OWNER:-}" || -z "${REPO:-}" ]]; then
|
|
detect_owner_repo
|
|
OWNER="${OWNER:-$DETECTED_OWNER}"
|
|
REPO="${REPO:-$DETECTED_REPO}"
|
|
fi
|
|
log_info "Using: ${OWNER}/${REPO}"
|
|
}
|
|
|
|
# ── Confirmation ─────────────────────────────────────────────────────
|
|
confirm() {
|
|
local msg="${1:-Proceed?}"
|
|
if [[ "${DRY_RUN:-false}" == "true" ]]; then
|
|
log_warn "[DRY RUN] Would execute: $msg"
|
|
return 1
|
|
fi
|
|
read -rp "$(echo -e "${YELLOW}$msg [y/N]${NC} ")" answer
|
|
[[ "$answer" =~ ^[Yy] ]]
|
|
}
|
|
|
|
# ── Date Helpers ─────────────────────────────────────────────────────
|
|
date_today() {
|
|
date +%Y-%m-%d
|
|
}
|
|
|
|
date_week_ago() {
|
|
date -d "7 days ago" +%Y-%m-%d 2>/dev/null || date -v-7d +%Y-%m-%d 2>/dev/null
|
|
}
|
|
|
|
date_month_ago() {
|
|
date -d "30 days ago" +%Y-%m-%d 2>/dev/null || date -v-30d +%Y-%m-%d 2>/dev/null
|
|
}
|
|
|
|
# ── Parameter Parsing ────────────────────────────────────────────────
|
|
parse_common_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--owner) OWNER="$2"; shift 2 ;;
|
|
--repo) REPO="$2"; shift 2 ;;
|
|
--dry-run) DRY_RUN="true"; shift ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ── Section Divider ──────────────────────────────────────────────────
|
|
divider() {
|
|
echo -e "${CYAN}────────────────────────────────────────────────${NC}"
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# Scientific Research Helpers
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
# Normalize value against max (returns 0-1, 0 if max is 0)
|
|
normalize() {
|
|
awk -v val="$1" -v max="$2" 'BEGIN { printf "%.4f", (max > 0) ? val / max : 0 }'
|
|
}
|
|
|
|
# Clamp value between lo and hi
|
|
clamp() {
|
|
awk -v val="$1" -v lo="$2" -v hi="$3" 'BEGIN { printf "%.4f", (val < lo) ? lo : ((val > hi) ? hi : val) }'
|
|
}
|
|
|
|
# Weighted sum: pass pairs of "value weight" as arguments
|
|
weighted_sum() {
|
|
awk 'BEGIN { sum=0; for(i=1;i<ARGC;i+=2) sum+=(ARGV[i]*ARGV[i+1]); printf "%.4f", sum }' "$@"
|
|
}
|
|
|
|
# Jaccard similarity between two space-separated strings
|
|
jaccard() {
|
|
echo "$1 $2" | awk '{
|
|
n1 = split($1, a1, " "); n2 = split($2, a2, " ");
|
|
delete seen; inter = 0;
|
|
for (i in a1) seen[a1[i]] = 1;
|
|
for (i in a2) if (seen[a2[i]] == 1) { inter++; seen[a2[i]] = 2; }
|
|
union = 0;
|
|
for (i in seen) union++;
|
|
printf "%.4f", (union > 0) ? inter / union : 0;
|
|
}'
|
|
}
|
|
|
|
# Detect programming language from file extension
|
|
detect_lang_from_ext() {
|
|
local ext="${1##*.}"
|
|
case "$ext" in
|
|
go) echo "Go" ;;
|
|
py|pyx) echo "Python" ;;
|
|
js|ts|jsx|tsx|mjs|cjs) echo "JavaScript/TypeScript" ;;
|
|
rs) echo "Rust" ;;
|
|
java) echo "Java" ;;
|
|
kt|kts) echo "Kotlin" ;;
|
|
c|cpp|cxx|h|hpp|hxx) echo "C/C++" ;;
|
|
r|R) echo "R" ;;
|
|
jl) echo "Julia" ;;
|
|
m|mm) echo "MATLAB/Objective-C" ;;
|
|
swift) echo "Swift" ;;
|
|
rb) echo "Ruby" ;;
|
|
php) echo "PHP" ;;
|
|
scala) echo "Scala" ;;
|
|
dart) echo "Dart" ;;
|
|
lua) echo "Lua" ;;
|
|
ipynb) echo "Jupyter Notebook" ;;
|
|
sh|bash|zsh) echo "Shell" ;;
|
|
ps1|psm1|psd1) echo "PowerShell" ;;
|
|
*) echo "Other" ;;
|
|
esac
|
|
}
|
|
|
|
# Cross-platform days between two dates (YYYY-MM-DD format)
|
|
days_between() {
|
|
local d1 d2 diff
|
|
d1=$(date -d "$1" +%s 2>/dev/null || date -jf "%Y-%m-%d" "$1" +%s 2>/dev/null || echo "0")
|
|
d2=$(date -d "$2" +%s 2>/dev/null || date -jf "%Y-%m-%d" "$2" +%s 2>/dev/null || echo "0")
|
|
diff=$(( (d2 - d1) / 86400 ))
|
|
echo "${diff#-}"
|
|
}
|
|
|
|
# Get today, N days ago (cross-platform)
|
|
date_days_ago() {
|
|
local n="$1"
|
|
date -d "$n days ago" +%Y-%m-%d 2>/dev/null || date -v-"$n"d +%Y-%m-%d 2>/dev/null
|
|
}
|
|
|
|
# Extract first N space-separated authors into BibTeX format
|
|
# Input: "First Last" "First2 Last2" ...
|
|
format_authors_bibtex() {
|
|
local names="$1" count=0 result=""
|
|
for name in $names; do
|
|
count=$((count + 1))
|
|
if [[ $count -gt 10 ]]; then
|
|
result="${result} and others"
|
|
break
|
|
fi
|
|
local last="${name##* }" first="${name%% *}"
|
|
[[ $count -gt 1 ]] && result="${result} and "
|
|
result="${result}${last}, ${first}"
|
|
done
|
|
echo "$result"
|
|
}
|
|
|
|
# Extract organization name from login (try git remote or repo +info)
|
|
org_from_owner() {
|
|
local owner="$1"
|
|
# check if it's an org or user by listing repos
|
|
local out
|
|
out=$(gl_run repo +list --user "$owner" --limit 1)
|
|
if [[ "$(json_ok "$out")" == "true" ]]; then
|
|
echo "$owner"
|
|
else
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
# Min and max helpers for awk
|
|
min_val() { awk -v a="$1" -v b="$2" 'BEGIN { print (a < b) ? a : b }'; }
|
|
max_val() { awk -v a="$1" -v b="$2" 'BEGIN { print (a > b) ? a : b }'; }
|
|
|
|
# ── Knowledge Graph Helpers ──────────────────────────────────────────
|
|
|
|
# Generate a unique node ID
|
|
kg_node_id() { echo "${1}:${2}" | tr '/' '_' | tr ' ' '_'; }
|
|
|
|
# URL-encode a string (basic)
|
|
url_encode() {
|
|
local str="$1"
|
|
echo "$str" | jq -sRr @uri 2>/dev/null || echo "$str"
|
|
}
|
|
|
|
# Escape JSON string value
|
|
json_escape() {
|
|
echo "$1" | jq -Rsa . 2>/dev/null || echo "\"$1\""
|
|
}
|
|
|
|
# ── Color-coded Severity ──────────────────────────────────────────────
|
|
|
|
severity_color() {
|
|
case "$1" in
|
|
Critical|critical|CRITICAL) echo -e "${RED}$1${NC}" ;;
|
|
Warning|warning|WARNING) echo -e "${YELLOW}$1${NC}" ;;
|
|
Info|info|INFO) echo -e "${CYAN}$1${NC}" ;;
|
|
OK|ok|CLEAN) echo -e "${GREEN}$1${NC}" ;;
|
|
*) echo "$1" ;;
|
|
esac
|
|
}
|