#!/usr/bin/env bash # Gitea Issues Utility (Bash) # # Prérequis: # - curl, jq # - Fichier .env à la racine du projet contenant au minimum: # GITEA_BASE_URL=https://git.example.com/api/v1 # GITEA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # GITEA_DEFAULT_OWNER=owner # GITEA_DEFAULT_REPO=repo # # Utilisation rapide: # ./gitea_issues.sh create -t "Titre" -b "Corps (Markdown)" [-F chemin/fichier.md] [-l label1,label2] [-a user1,user2] # ./gitea_issues.sh list [-s open|closed|all] [-p page] [-P limit] # ./gitea_issues.sh get # ./gitea_issues.sh update [-t titre] [-b corps (Markdown) | -F chemin/fichier.md] [-l labels] [-a assignees] # ./gitea_issues.sh close # ./gitea_issues.sh open # ./gitea_issues.sh comment -m "message (Markdown)" [-M chemin/commentaire.md] # # Notes: # - Les scripts lisent ../.env (depuis ./.utlis/) automatiquement. # - Vous pouvez surcharger owner/repo via variables d'env OWNER/REPO ou flags --owner/--repo. set -euo pipefail # Assurer un environnement UTF-8 pour les E/S et curl/jq ensure_utf8_locale() { # Si LANG/LC_ALL ne sont pas déjà en UTF-8, essaye de basculer local want="UTF-8" if [[ "${LANG:-}" != *"$want"* || "${LC_ALL:-}" != *"$want"* ]]; then local chosen="" if command -v locale >/dev/null 2>&1; then if locale -a 2>/dev/null | grep -qi '^C\.utf8\|C\.UTF-8$'; then chosen="C.UTF-8" elif locale -a 2>/dev/null | grep -qi '^en_US\.utf8\|en_US\.UTF-8$'; then chosen="en_US.UTF-8" elif locale -a 2>/dev/null | grep -qi '^fr_FR\.utf8\|fr_FR\.UTF-8$'; then chosen="fr_FR.UTF-8" fi fi if [[ -z "$chosen" ]]; then # Fallback raisonnable si locale -a n'est pas dispo chosen="C.UTF-8" fi export LANG="$chosen" export LC_ALL="$chosen" fi } ensure_utf8_locale SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Charger .env si présent ENV_FILE="$REPO_ROOT/.env" if [[ -f "$ENV_FILE" ]]; then # shellcheck disable=SC2046 export $(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$ENV_FILE" | sed 's/#.*//') fi BASE_URL="${GITEA_BASE_URL:-}" TOKEN="${GITEA_TOKEN:-}" DEFAULT_OWNER="${GITEA_DEFAULT_OWNER:-}" DEFAULT_REPO="${GITEA_DEFAULT_REPO:-}" OWNER_OVERRIDE="" REPO_OVERRIDE="" require_tools() { for t in curl jq; do command -v "$t" >/dev/null 2>&1 || { echo "Erreur: outil requis introuvable: $t" >&2; exit 1; } done } usage() { sed -n '1,60p' "$0" } fail() { echo "Erreur: $*" >&2; exit 1; } ensure_env() { [[ -n "$BASE_URL" ]] || fail "GITEA_BASE_URL manquant (.env)" [[ -n "$TOKEN" ]] || fail "GITEA_TOKEN manquant (.env)" } resolve_repo() { local owner repo owner="${OWNER_OVERRIDE:-${OWNER:-$DEFAULT_OWNER}}" repo="${REPO_OVERRIDE:-${REPO:-$DEFAULT_REPO}}" [[ -n "$owner" && -n "$repo" ]] || fail "OWNER/REPO non définis (utilisez .env ou --owner/--repo)" echo "$owner" "$repo" } api() { local method="$1" path="$2"; shift 2 local url="$BASE_URL$path" curl -sS -X "$method" \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/json; charset=utf-8" \ -H "Accept-Charset: utf-8" \ "$url" "$@" } api_jq_or_status() { # Lit la sortie JSON et vérifie erreurs Gitea local http_code json # Utilise curl avec -w pour capturer le code json=$(curl -sS -w '\n%{http_code}' \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/json; charset=utf-8" \ -H "Accept-Charset: utf-8" \ "$@") http_code="${json##*$'\n'}" json="${json%$'\n'*}" if [[ "$http_code" -ge 400 ]]; then echo "$json" | jq . 2>/dev/null || true fail "HTTP $http_code" else echo "$json" | jq . fi } parse_common_flags() { while [[ $# -gt 0 ]]; do case "$1" in --owner) OWNER_OVERRIDE="$2"; shift 2;; --repo) REPO_OVERRIDE="$2"; shift 2;; -h|--help) usage; exit 0;; *) break;; esac done echo "$@" } cmd_create() { local title="" body="" body_file="" labels="" assignees="" while [[ $# -gt 0 ]]; do case "$1" in -t|--title) title="$2"; shift 2;; -b|--body) body="$2"; shift 2;; -F|--body-file) body_file="$2"; shift 2;; -l|--labels) labels="$2"; shift 2;; -a|--assignees) assignees="$2"; shift 2;; *) break;; esac done [[ -n "$title" ]] || fail "Titre requis (-t)" read -r owner repo < <(resolve_repo) local data if [[ -n "$body" ]]; then # Inline prioritaire data=$(jq -n \ --arg title "$title" \ --arg body "$body" \ --arg labels_csv "$labels" \ --arg assignees_csv "$assignees" ' { title: $title, body: $body } + ( ($labels_csv|length) > 0 then { labels: ($labels_csv | split(",") | map(select(. != ""))) } else {} end) + ( ($assignees_csv|length) > 0 then { assignees: ($assignees_csv | split(",") | map(select(. != ""))) } else {} end) ') if [[ -n "$body_file" ]]; then echo "Avertissement: -b et -F fournis; -b (inline) sera prioritaire" >&2 fi elif [[ -n "$body_file" ]]; then [[ -f "$body_file" ]] || fail "Fichier introuvable: $body_file" data=$(jq -n \ --arg title "$title" \ --arg labels_csv "$labels" \ --arg assignees_csv "$assignees" \ --rawfile body "$body_file" ' { title: $title, body: $body } + ( ($labels_csv|length) > 0 then { labels: ($labels_csv | split(",") | map(select(. != ""))) } else {} end) + ( ($assignees_csv|length) > 0 then { assignees: ($assignees_csv | split(",") | map(select(. != ""))) } else {} end) ') else data=$(jq -n \ --arg title "$title" ' { title: $title } ') fi api_jq_or_status -X POST "$BASE_URL/repos/$owner/$repo/issues" --data-binary "$data" } cmd_list() { local state="open" page="1" limit="30" while [[ $# -gt 0 ]]; do case "$1" in -s|--state) state="$2"; shift 2;; -p|--page) page="$2"; shift 2;; -P|--limit) limit="$2"; shift 2;; *) break;; esac done read -r owner repo < <(resolve_repo) api GET "/repos/$owner/$repo/issues?state=$state&page=$page&limit=$limit" | jq '.[].number as $n | {number: $n, title: .title, state: .state, labels: [.labels[].name]}' } cmd_get() { local index="$1"; [[ -n "$index" ]] || fail "Index requis" read -r owner repo < <(resolve_repo) api GET "/repos/$owner/$repo/issues/$index" | jq '{number, title, state, body, labels: [.labels[].name], assignees: [.assignees[].login], created_at, updated_at}' } cmd_update() { local index title="" body="" body_file="" labels="" assignees="" state="" index="$1"; shift || true [[ -n "$index" ]] || fail "Index requis" while [[ $# -gt 0 ]]; do case "$1" in -t|--title) title="$2"; shift 2;; -b|--body) body="$2"; shift 2;; -F|--body-file) body_file="$2"; shift 2;; -l|--labels) labels="$2"; shift 2;; -a|--assignees) assignees="$2"; shift 2;; -s|--state) state="$2"; shift 2;; *) break;; esac done read -r owner repo < <(resolve_repo) local data if [[ -n "$body" ]]; then data=$(jq -n \ --arg title "$title" \ --arg body "$body" \ --arg labels_csv "$labels" \ --arg assignees_csv "$assignees" \ --arg state "$state" ' {} + ( ($title|length)>0 then {title:$title} else {} end ) + {body:$body} + ( ($labels_csv|length)>0 then {labels: ($labels_csv|split(",")|map(select(.!="")))} else {} end ) + ( ($assignees_csv|length)>0 then {assignees: ($assignees_csv|split(",")|map(select(.!="")))} else {} end ) + ( ($state|length)>0 then {state:$state} else {} end ) ') if [[ -n "$body_file" ]]; then echo "Avertissement: -b et -F fournis; -b (inline) sera prioritaire" >&2 fi elif [[ -n "$body_file" ]]; then [[ -f "$body_file" ]] || fail "Fichier introuvable: $body_file" data=$(jq -n \ --arg title "$title" \ --rawfile body "$body_file" \ --arg labels_csv "$labels" \ --arg assignees_csv "$assignees" \ --arg state "$state" ' {} + ( ($title|length)>0 then {title:$title} else {} end ) + {body:$body} + ( ($labels_csv|length)>0 then {labels: ($labels_csv|split(",")|map(select(.!="")))} else {} end ) + ( ($assignees_csv|length)>0 then {assignees: ($assignees_csv|split(",")|map(select(.!="")))} else {} end ) + ( ($state|length)>0 then {state:$state} else {} end ) ') else data=$(jq -n \ --arg title "$title" ' {} + ( ($title|length)>0 then {title:$title} else {} end ) ') fi api_jq_or_status -X PATCH "$BASE_URL/repos/$owner/$repo/issues/$index" --data-binary "$data" } cmd_close() { local index="$1"; [[ -n "$index" ]] || fail "Index requis" cmd_update "$index" -s closed } cmd_open() { local index="$1"; [[ -n "$index" ]] || fail "Index requis" cmd_update "$index" -s open } cmd_comment() { local index message="" message_file="" index="$1"; shift || true [[ -n "$index" ]] || fail "Index requis" while [[ $# -gt 0 ]]; do case "$1" in -m|--message) message="$2"; shift 2;; -M|--message-file) message_file="$2"; shift 2;; *) break;; esac done # Inline prioritaire if [[ -n "$message" ]]; then read -r owner repo < <(resolve_repo) local data data=$(jq -n --arg body "$message" '{body:$body}') if [[ -n "$message_file" ]]; then echo "Avertissement: -m et -M fournis; -m (inline) sera prioritaire" >&2 fi api_jq_or_status -X POST "$BASE_URL/repos/$owner/$repo/issues/$index/comments" --data-binary "$data" return elif [[ -n "$message_file" ]]; then [[ -f "$message_file" ]] || fail "Fichier introuvable: $message_file" read -r owner repo < <(resolve_repo) local data data=$(jq -n --rawfile body "$message_file" '{body:$body}') api_jq_or_status -X POST "$BASE_URL/repos/$owner/$repo/issues/$index/comments" --data-binary "$data" return else fail "Message requis (-m) ou fichier (-M)" fi } main() { require_tools ensure_env local args args=( $(parse_common_flags "$@") ) || true set +u local cmd="${args[0]}"; shift || true set -u case "$cmd" in create) shift; cmd_create "$@" ;; list) shift; cmd_list "$@" ;; get) shift; cmd_get "$1" ;; update) shift; cmd_update "$@" ;; close) shift; cmd_close "$1" ;; open) shift; cmd_open "$1" ;; comment) shift; cmd_comment "$@" ;; -h|--help|help|""|*) usage; exit 1;; esac } main "$@"