<# 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 }