From 775c0ea210351130c68b039c516900f35a48cc97 Mon Sep 17 00:00:00 2001 From: Avnyr Date: Sun, 4 Jan 2026 23:33:04 +0100 Subject: [PATCH] Add Gitea issue management utilities for Bash and PowerShell scripts --- .utlis/gitea_issues.ps1 | 303 +++++++++++++++++++++++++++++++++++ .utlis/gitea_issues.sh | 339 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 642 insertions(+) create mode 100644 .utlis/gitea_issues.ps1 create mode 100644 .utlis/gitea_issues.sh diff --git a/.utlis/gitea_issues.ps1 b/.utlis/gitea_issues.ps1 new file mode 100644 index 0000000..9104a1a --- /dev/null +++ b/.utlis/gitea_issues.ps1 @@ -0,0 +1,303 @@ +<# + Gitea Issues Utility (PowerShell) + + Prérequis: + - PowerShell 5+ (ou 7+) + - Fichier .env à la racine du dépôt avec: + GITEA_BASE_URL=https://git.example.com/api/v1 + GITEA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + GITEA_DEFAULT_OWNER=owner + GITEA_DEFAULT_REPO=repo + + Utilisation rapide: + .\gitea_issues.ps1 create -Title "Titre" \ + [-Body "CorpsMarkdown" | -BodyFromStdin | -BodyFile chemin\\fichier.md | -BodyBase64 "..." ] \ + -Labels "bug,help wanted" -Assignees "user1,user2" [-Owner o] [-Repo r] + .\gitea_issues.ps1 list [-State open|closed|all] [-Page 1] [-Limit 30] [-Owner o] [-Repo r] + .\gitea_issues.ps1 get -Index 12 [-Owner o] [-Repo r] + .\gitea_issues.ps1 update -Index 12 [-Title x] \ + [-Body y | -BodyFromStdin | -BodyFile chemin\\fichier.md | -BodyBase64 "..."] \ + [-Labels csv] [-Assignees csv] [-State open|closed] [-Owner o] [-Repo r] + .\gitea_issues.ps1 close -Index 12 [-Owner o] [-Repo r] + .\gitea_issues.ps1 open -Index 12 [-Owner o] [-Repo r] + .\gitea_issues.ps1 comment -Index 12 \ + [-Message "TexteMarkdown" | -MessageFromStdin | -MessageFile chemin\\commentaire.md | -MessageBase64 "..."] \ + [-Owner o] [-Repo r] + + Priorité des sources de contenu (inline prioritaire): + Body: -Body > -BodyFromStdin > -BodyFile > -BodyBase64 + Message: -Message > -MessageFromStdin > -MessageFile > -MessageBase64 + Astuce: pour éviter les problèmes d'échappement avec les ``` en PowerShell, utilisez un here-string + ou passez le contenu via un pipe et -BodyFromStdin / -MessageFromStdin. +#> + +[CmdletBinding(DefaultParameterSetName='help')] +param( + [Parameter(ParameterSetName='create')][switch]$create, + [Parameter(ParameterSetName='list')][switch]$list, + [Parameter(ParameterSetName='get')][switch]$get, + [Parameter(ParameterSetName='update')][switch]$update, + [Parameter(ParameterSetName='close')][switch]$close, + [Parameter(ParameterSetName='open')][switch]$open, + [Parameter(ParameterSetName='comment')][switch]$comment, + + # communs + [string]$Owner, + [string]$Repo, + + # create/update + [string]$Title, + [string]$Body, + [switch]$BodyFromStdin, + [string]$BodyFile, + [string]$BodyBase64, + [string]$Labels, + [string]$Assignees, + [ValidateSet('open','closed')][string]$State, + + # list + [ValidateSet('open','closed','all')][string]$StateList='open', + [int]$Page=1, + [int]$Limit=30, + + # get/update/close/open/comment + [int]$Index, + + # comment + [string]$Message, + [switch]$MessageFromStdin, + [string]$MessageFile, + [string]$MessageBase64 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Capture éventuellement l'entrée du pipeline vers le script +# (permet d'utiliser: Get-Content -Raw file | .\gitea_issues.ps1 ... -BodyFromStdin) +try { + $script:__pipedInput = $null + # $input est un énumérateur; si le script reçoit un pipe, on le matérialise en texte + $script:__pipedInput = ($input | Out-String) + if ([string]::IsNullOrWhiteSpace($script:__pipedInput)) { $script:__pipedInput = $null } +} catch { $script:__pipedInput = $null } + +function Load-Env { + # Détection robuste du dossier du script (compatibilité pwsh/host) + $scriptDir = $null + if ($PSScriptRoot) { + $scriptDir = $PSScriptRoot + } elseif ($PSCommandPath) { + $scriptDir = Split-Path -Parent $PSCommandPath + } elseif ($MyInvocation -and $MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) { + $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + } else { + $scriptDir = (Get-Location).Path + } + $repoRoot = Resolve-Path (Join-Path $scriptDir '..') + $envFile = Join-Path $repoRoot '.env' + if (Test-Path $envFile) { + Get-Content $envFile | ForEach-Object { + if ($_ -match '^[A-Za-z_][A-Za-z0-9_]*=') { + $kv = $_ -replace '\r','' + $name,$value = $kv.Split('=',2) + if ($null -ne $value) { + $value = $value.Trim() + if ($value.StartsWith('"') -and $value.EndsWith('"') -and $value.Length -ge 2) { + $value = $value.Substring(1, $value.Length - 2) + } + Set-Item -Path ("Env:{0}" -f $name) -Value $value + } + } + } + } +} + +function Ensure-UTF8 { + try { + # Forcer l'encodage console en UTF-8 (sans BOM) + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [Console]::OutputEncoding = $utf8NoBom + [Console]::InputEncoding = $utf8NoBom + $script:OutputEncoding = $utf8NoBom + $global:OutputEncoding = $utf8NoBom + # Sur Windows, fixer la page de code à 65001 si possible (silencieux) + if ($IsWindows) { cmd /c chcp 65001 > $null 2>&1 } + } catch { } +} + +function Ensure-Env { + if (-not $env:GITEA_BASE_URL) { throw 'GITEA_BASE_URL manquant dans .env' } + if (-not $env:GITEA_TOKEN) { throw 'GITEA_TOKEN manquant dans .env' } +} + +function Decode-Base64Utf8 { + param([string]$b64) + if (-not $b64) { return $null } + try { + $bytes = [Convert]::FromBase64String($b64) + return [System.Text.Encoding]::UTF8.GetString($bytes) + } catch { + throw 'Chaîne Base64 invalide' + } +} + +function Read-AllStdin { + try { + # 1) Si l'entrée provient du pipeline (capturée au niveau script) + if ($script:__pipedInput) { return $script:__pipedInput } + # 2) Sinon, lecture directe de STDIN (pipe vers PowerShell) + $content = $null + try { $content = [Console]::In.ReadToEnd() } catch { $content = $null } + return $content + } catch { + return $null + } +} + +function Resolve-Content { + param( + [string]$Inline, + [switch]$FromStdin, + [string]$FromFile, + [string]$FromBase64, + [string]$Kind # 'Body' | 'Message' + ) + + # Avertissements de priorité si plusieurs sources sont présentes + $provided = @() + if ($Inline) { $provided += 'Inline' } + if ($FromStdin) { $provided += 'Stdin' } + if ($FromFile) { $provided += 'File' } + if ($FromBase64) { $provided += 'Base64' } + if ($provided.Count -gt 1) { + Write-Warning ("Plusieurs sources pour {0} détectées ({1}); la priorité est Inline > Stdin > File > Base64." -f $Kind, ($provided -join ', ')) + } + + if ($Inline) { return $Inline } + if ($FromStdin) { + $stdin = Read-AllStdin + if ($stdin) { return $stdin } + Write-Warning ("{0} demandé depuis stdin mais aucun contenu lu; on continue sur la source suivante." -f $Kind) + } + if ($FromFile) { + if (-not (Test-Path -Path $FromFile -PathType Leaf)) { throw "Fichier introuvable: $FromFile" } + return Get-Content -LiteralPath $FromFile -Raw -Encoding UTF8 + } + if ($FromBase64) { return Decode-Base64Utf8 $FromBase64 } + return $null +} + +function Resolve-Repo { + param([string]$OwnerIn,[string]$RepoIn) + $o = if ($OwnerIn) { $OwnerIn } elseif ($env:OWNER) { $env:OWNER } elseif ($env:GITEA_DEFAULT_OWNER) { $env:GITEA_DEFAULT_OWNER } else { '' } + $r = if ($RepoIn) { $RepoIn } elseif ($env:REPO) { $env:REPO } elseif ($env:GITEA_DEFAULT_REPO) { $env:GITEA_DEFAULT_REPO } else { '' } + if (-not $o -or -not $r) { throw 'OWNER/REPO non définis (utilisez .env ou paramètres -Owner/-Repo)' } + return @($o,$r) +} + +function Invoke-Gitea { + param( + [ValidateSet('GET','POST','PATCH','PUT','DELETE')][string]$Method, + [string]$Path, + $BodyObj + ) + $base = $env:GITEA_BASE_URL.TrimEnd('/') + $uri = "$base$Path" + $headers = @{ Authorization = "token $($env:GITEA_TOKEN)" } + if ($PSBoundParameters.ContainsKey('BodyObj') -and $BodyObj -ne $null) { + $json = $BodyObj | ConvertTo-Json -Depth 10 + # Encodage explicite UTF-8 pour le corps JSON + $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) + $headers['Content-Type'] = 'application/json; charset=utf-8' + return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -Body $bytes + } else { + return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers + } +} + +function Write-Help { + Get-Content -TotalCount 40 $MyInvocation.MyCommand.Path | ForEach-Object { $_ } +} + +try { + Ensure-UTF8 + Load-Env + Ensure-Env + + if ($create) { + if (-not $Title) { throw 'Paramètre -Title requis' } + $owner,$repo = Resolve-Repo -OwnerIn $Owner -RepoIn $Repo + $Body = Resolve-Content -Inline $Body -FromStdin:$BodyFromStdin -FromFile $BodyFile -FromBase64 $BodyBase64 -Kind 'Body' + $bodyObj = @{ title = $Title } + if ($Body) { $bodyObj.body = $Body } + if ($Labels) { $bodyObj.labels = ($Labels -split ',') | Where-Object { $_ -ne '' } } + if ($Assignees) { $bodyObj.assignees = ($Assignees -split ',') | Where-Object { $_ -ne '' } } + $res = Invoke-Gitea -Method POST -Path "/repos/$owner/$repo/issues" -BodyObj $bodyObj + $res | ConvertTo-Json -Depth 10 | Write-Output + return + } + + if ($list) { + $owner,$repo = Resolve-Repo -OwnerIn $Owner -RepoIn $Repo + $res = Invoke-Gitea -Method GET -Path "/repos/$owner/$repo/issues?state=$StateList&page=$Page&limit=$Limit" + $res | ForEach-Object { [PSCustomObject]@{ number = $_.number; title = $_.title; state = $_.state; labels = ($_.labels | ForEach-Object { $_.name }) -join ',' } } | Format-Table -AutoSize + return + } + + if ($get) { + if (-not $Index) { throw 'Paramètre -Index requis' } + $owner,$repo = Resolve-Repo -OwnerIn $Owner -RepoIn $Repo + $res = Invoke-Gitea -Method GET -Path "/repos/$owner/$repo/issues/$Index" + $res | ConvertTo-Json -Depth 10 | Write-Output + return + } + + if ($update) { + if (-not $Index) { throw 'Paramètre -Index requis' } + $owner,$repo = Resolve-Repo -OwnerIn $Owner -RepoIn $Repo + $Body = Resolve-Content -Inline $Body -FromStdin:$BodyFromStdin -FromFile $BodyFile -FromBase64 $BodyBase64 -Kind 'Body' + $bodyObj = @{} + if ($Title) { $bodyObj.title = $Title } + if ($Body) { $bodyObj.body = $Body } + if ($Labels) { $bodyObj.labels = ($Labels -split ',') | Where-Object { $_ -ne '' } } + if ($Assignees) { $bodyObj.assignees = ($Assignees -split ',') | Where-Object { $_ -ne '' } } + if ($State) { $bodyObj.state = $State } + if ($bodyObj.Keys.Count -eq 0) { throw 'Aucun champ à mettre à jour' } + $res = Invoke-Gitea -Method PATCH -Path "/repos/$owner/$repo/issues/$Index" -BodyObj $bodyObj + $res | ConvertTo-Json -Depth 10 | Write-Output + return + } + + if ($close) { + if (-not $Index) { throw 'Paramètre -Index requis' } + $owner,$repo = Resolve-Repo -OwnerIn $Owner -RepoIn $Repo + $res = Invoke-Gitea -Method PATCH -Path "/repos/$owner/$repo/issues/$Index" -BodyObj @{ state = 'closed' } + $res | ConvertTo-Json -Depth 10 | Write-Output + return + } + + if ($open) { + if (-not $Index) { throw 'Paramètre -Index requis' } + $owner,$repo = Resolve-Repo -OwnerIn $Owner -RepoIn $Repo + $res = Invoke-Gitea -Method PATCH -Path "/repos/$owner/$repo/issues/$Index" -BodyObj @{ state = 'open' } + $res | ConvertTo-Json -Depth 10 | Write-Output + return + } + + if ($comment) { + if (-not $Index) { throw 'Paramètre -Index requis' } + $Message = Resolve-Content -Inline $Message -FromStdin:$MessageFromStdin -FromFile $MessageFile -FromBase64 $MessageBase64 -Kind 'Message' + if (-not $Message) { throw 'Paramètre -Message (ou -MessageFromStdin / -MessageFile / -MessageBase64) requis' } + $owner,$repo = Resolve-Repo -OwnerIn $Owner -RepoIn $Repo + $res = Invoke-Gitea -Method POST -Path "/repos/$owner/$repo/issues/$Index/comments" -BodyObj @{ body = $Message } + $res | ConvertTo-Json -Depth 10 | Write-Output + return + } + + Write-Help +} +catch { + Write-Error $_ + exit 1 +} diff --git a/.utlis/gitea_issues.sh b/.utlis/gitea_issues.sh new file mode 100644 index 0000000..146ade3 --- /dev/null +++ b/.utlis/gitea_issues.sh @@ -0,0 +1,339 @@ +#!/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 "$@"