feat: rename rails/ to tools/ and add service tool suites (#4)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #4.
This commit is contained in:
247
tools/git/ci-queue-wait.ps1
Normal file
247
tools/git/ci-queue-wait.ps1
Normal file
@@ -0,0 +1,247 @@
|
||||
# ci-queue-wait.ps1 - Wait until project CI queue is clear (no running/queued pipeline on branch head)
|
||||
# Usage: .\ci-queue-wait.ps1 [-Branch main] [-TimeoutSeconds 900] [-IntervalSeconds 15] [-Purpose merge] [-RequireStatus]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("B")]
|
||||
[string]$Branch = "main",
|
||||
|
||||
[Alias("t")]
|
||||
[int]$TimeoutSeconds = 900,
|
||||
|
||||
[Alias("i")]
|
||||
[int]$IntervalSeconds = 15,
|
||||
|
||||
[ValidateSet("push", "merge")]
|
||||
[string]$Purpose = "merge",
|
||||
|
||||
[switch]$RequireStatus,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: ci-queue-wait.ps1 [-Branch main] [-TimeoutSeconds 900] [-IntervalSeconds 15] [-Purpose push|merge] [-RequireStatus]
|
||||
|
||||
Options:
|
||||
-Branch, -B BRANCH Branch head to inspect (default: main)
|
||||
-TimeoutSeconds, -t SECONDS Max wait time (default: 900)
|
||||
-IntervalSeconds, -i SECONDS Poll interval (default: 15)
|
||||
-Purpose VALUE push or merge (default: merge)
|
||||
-RequireStatus Fail if no CI status contexts are present
|
||||
-Help, -h Show help
|
||||
"@
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($TimeoutSeconds -lt 1 -or $IntervalSeconds -lt 1) {
|
||||
Write-Error "TimeoutSeconds and IntervalSeconds must be positive integers."
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Get-RemoteHost {
|
||||
$remoteUrl = git remote get-url origin 2>$null
|
||||
if ([string]::IsNullOrEmpty($remoteUrl)) { return $null }
|
||||
if ($remoteUrl -match "^https?://([^/]+)/") { return $Matches[1] }
|
||||
if ($remoteUrl -match "^git@([^:]+):") { return $Matches[1] }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-GiteaToken {
|
||||
param([string]$Host)
|
||||
|
||||
if ($env:GITEA_TOKEN) { return $env:GITEA_TOKEN }
|
||||
|
||||
$credPath = Join-Path $HOME ".git-credentials"
|
||||
if (-not (Test-Path $credPath)) { return $null }
|
||||
|
||||
$line = Get-Content $credPath | Where-Object { $_ -like "*$Host*" } | Select-Object -First 1
|
||||
if (-not $line) { return $null }
|
||||
|
||||
if ($line -match 'https?://[^@]*:([^@/]+)@') {
|
||||
return $Matches[1]
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-QueueState {
|
||||
param([object]$Payload)
|
||||
|
||||
$pending = @("pending", "queued", "running", "waiting")
|
||||
$failure = @("failure", "error", "failed")
|
||||
$success = @("success")
|
||||
|
||||
$state = ""
|
||||
if ($null -ne $Payload.state) {
|
||||
$state = "$($Payload.state)".ToLowerInvariant()
|
||||
}
|
||||
|
||||
if ($pending -contains $state) { return "pending" }
|
||||
if ($failure -contains $state) { return "terminal-failure" }
|
||||
if ($success -contains $state) { return "terminal-success" }
|
||||
|
||||
$values = @()
|
||||
$statuses = @()
|
||||
if ($null -ne $Payload.statuses) { $statuses = @($Payload.statuses) }
|
||||
|
||||
foreach ($s in $statuses) {
|
||||
if ($null -eq $s) { continue }
|
||||
$v = ""
|
||||
if ($null -ne $s.status) { $v = "$($s.status)".ToLowerInvariant() }
|
||||
elseif ($null -ne $s.state) { $v = "$($s.state)".ToLowerInvariant() }
|
||||
if (-not [string]::IsNullOrEmpty($v)) { $values += $v }
|
||||
}
|
||||
|
||||
if ($values.Count -eq 0 -and [string]::IsNullOrEmpty($state)) { return "no-status" }
|
||||
if (($values | Where-Object { $pending -contains $_ }).Count -gt 0) { return "pending" }
|
||||
if (($values | Where-Object { $failure -contains $_ }).Count -gt 0) { return "terminal-failure" }
|
||||
if ($values.Count -gt 0 -and ($values | Where-Object { -not ($success -contains $_) }).Count -eq 0) { return "terminal-success" }
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
function Print-PendingContexts {
|
||||
param([object]$Payload)
|
||||
|
||||
$pending = @("pending", "queued", "running", "waiting")
|
||||
$statuses = @()
|
||||
if ($null -ne $Payload.statuses) { $statuses = @($Payload.statuses) }
|
||||
|
||||
if ($statuses.Count -eq 0) {
|
||||
Write-Host "[ci-queue-wait] no status contexts reported"
|
||||
return
|
||||
}
|
||||
|
||||
$found = $false
|
||||
foreach ($s in $statuses) {
|
||||
if ($null -eq $s) { continue }
|
||||
$name = if ($s.context) { $s.context } elseif ($s.name) { $s.name } else { "unknown-context" }
|
||||
$value = if ($s.status) { "$($s.status)".ToLowerInvariant() } elseif ($s.state) { "$($s.state)".ToLowerInvariant() } else { "unknown" }
|
||||
$target = if ($s.target_url) { $s.target_url } elseif ($s.url) { $s.url } else { "" }
|
||||
if ($pending -contains $value) {
|
||||
$found = $true
|
||||
if ($target) {
|
||||
Write-Host "[ci-queue-wait] pending: $name=$value ($target)"
|
||||
}
|
||||
else {
|
||||
Write-Host "[ci-queue-wait] pending: $name=$value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "[ci-queue-wait] no pending contexts"
|
||||
}
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
$owner = Get-GitRepoOwner
|
||||
$repo = Get-GitRepoName
|
||||
|
||||
if ([string]::IsNullOrEmpty($owner) -or [string]::IsNullOrEmpty($repo)) {
|
||||
Write-Error "Could not determine repository owner/name from git remote."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$headSha = $null
|
||||
$host = $null
|
||||
$giteaToken = $null
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "gh CLI is required for GitHub CI queue guard."
|
||||
exit 1
|
||||
}
|
||||
$headSha = (& gh api "repos/$owner/$repo/branches/$Branch" --jq ".commit.sha").Trim()
|
||||
if ([string]::IsNullOrEmpty($headSha)) {
|
||||
Write-Error "Could not resolve $Branch head SHA."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[ci-queue-wait] platform=github purpose=$Purpose branch=$Branch sha=$headSha"
|
||||
}
|
||||
"gitea" {
|
||||
$host = Get-RemoteHost
|
||||
if ([string]::IsNullOrEmpty($host)) {
|
||||
Write-Error "Could not determine remote host."
|
||||
exit 1
|
||||
}
|
||||
$giteaToken = Get-GiteaToken -Host $host
|
||||
if ([string]::IsNullOrEmpty($giteaToken)) {
|
||||
Write-Error "Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials."
|
||||
exit 1
|
||||
}
|
||||
try {
|
||||
$branchUrl = "https://$host/api/v1/repos/$owner/$repo/branches/$Branch"
|
||||
$branchPayload = Invoke-RestMethod -Method Get -Uri $branchUrl -Headers @{ Authorization = "token $giteaToken" }
|
||||
$headSha = ($branchPayload.commit.id | Out-String).Trim()
|
||||
}
|
||||
catch {
|
||||
Write-Error "Could not resolve $Branch head SHA from Gitea API."
|
||||
exit 1
|
||||
}
|
||||
if ([string]::IsNullOrEmpty($headSha)) {
|
||||
Write-Error "Could not resolve $Branch head SHA."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[ci-queue-wait] platform=gitea purpose=$Purpose branch=$Branch sha=$headSha"
|
||||
}
|
||||
default {
|
||||
Write-Error "Unsupported platform '$platform'."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
|
||||
while ($true) {
|
||||
if ((Get-Date) -gt $deadline) {
|
||||
Write-Error "Timed out waiting for CI queue to clear on $Branch after ${TimeoutSeconds}s."
|
||||
exit 124
|
||||
}
|
||||
|
||||
try {
|
||||
if ($platform -eq "github") {
|
||||
$statusJson = & gh api "repos/$owner/$repo/commits/$headSha/status"
|
||||
$payload = $statusJson | ConvertFrom-Json
|
||||
}
|
||||
else {
|
||||
$statusUrl = "https://$host/api/v1/repos/$owner/$repo/commits/$headSha/status"
|
||||
$payload = Invoke-RestMethod -Method Get -Uri $statusUrl -Headers @{ Authorization = "token $giteaToken" }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Failed to query commit status for queue guard."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$state = Get-QueueState -Payload $payload
|
||||
Write-Host "[ci-queue-wait] state=$state purpose=$Purpose branch=$Branch"
|
||||
|
||||
switch ($state) {
|
||||
"pending" {
|
||||
Print-PendingContexts -Payload $payload
|
||||
Start-Sleep -Seconds $IntervalSeconds
|
||||
}
|
||||
"no-status" {
|
||||
if ($RequireStatus) {
|
||||
Write-Error "No CI status contexts found while -RequireStatus is set."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[ci-queue-wait] no status contexts present; proceeding."
|
||||
exit 0
|
||||
}
|
||||
"terminal-success" { exit 0 }
|
||||
"terminal-failure" { exit 0 }
|
||||
"unknown" { exit 0 }
|
||||
default { exit 0 }
|
||||
}
|
||||
}
|
||||
307
tools/git/ci-queue-wait.sh
Executable file
307
tools/git/ci-queue-wait.sh
Executable file
@@ -0,0 +1,307 @@
|
||||
#!/bin/bash
|
||||
# ci-queue-wait.sh - Wait until project CI queue is clear (no running/queued pipeline on branch head)
|
||||
# Usage: ci-queue-wait.sh [-B branch] [-t timeout_sec] [-i interval_sec] [--purpose push|merge] [--require-status]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
BRANCH="main"
|
||||
TIMEOUT_SEC=900
|
||||
INTERVAL_SEC=15
|
||||
PURPOSE="merge"
|
||||
REQUIRE_STATUS=0
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [-B branch] [-t timeout_sec] [-i interval_sec] [--purpose push|merge] [--require-status]
|
||||
|
||||
Options:
|
||||
-B, --branch BRANCH Branch head to inspect (default: main)
|
||||
-t, --timeout SECONDS Max wait time in seconds (default: 900)
|
||||
-i, --interval SECONDS Poll interval in seconds (default: 15)
|
||||
--purpose VALUE Log context: push|merge (default: merge)
|
||||
--require-status Fail if no CI status contexts are present
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0")
|
||||
$(basename "$0") --purpose push -B main -t 600 -i 10
|
||||
EOF
|
||||
}
|
||||
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_state_from_status_json() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print("unknown")
|
||||
raise SystemExit(0)
|
||||
|
||||
statuses = payload.get("statuses") or []
|
||||
state = (payload.get("state") or "").lower()
|
||||
|
||||
pending_values = {"pending", "queued", "running", "waiting"}
|
||||
failure_values = {"failure", "error", "failed"}
|
||||
success_values = {"success"}
|
||||
|
||||
if state in pending_values:
|
||||
print("pending")
|
||||
raise SystemExit(0)
|
||||
if state in failure_values:
|
||||
print("terminal-failure")
|
||||
raise SystemExit(0)
|
||||
if state in success_values:
|
||||
print("terminal-success")
|
||||
raise SystemExit(0)
|
||||
|
||||
values = []
|
||||
for item in statuses:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
value = (item.get("status") or item.get("state") or "").lower()
|
||||
if value:
|
||||
values.append(value)
|
||||
|
||||
if not values and not state:
|
||||
print("no-status")
|
||||
elif any(v in pending_values for v in values):
|
||||
print("pending")
|
||||
elif any(v in failure_values for v in values):
|
||||
print("terminal-failure")
|
||||
elif values and all(v in success_values for v in values):
|
||||
print("terminal-success")
|
||||
else:
|
||||
print("unknown")
|
||||
PY
|
||||
}
|
||||
|
||||
print_pending_contexts() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print("[ci-queue-wait] unable to decode status payload")
|
||||
raise SystemExit(0)
|
||||
|
||||
statuses = payload.get("statuses") or []
|
||||
if not statuses:
|
||||
print("[ci-queue-wait] no status contexts reported")
|
||||
raise SystemExit(0)
|
||||
|
||||
pending_values = {"pending", "queued", "running", "waiting"}
|
||||
found = False
|
||||
for item in statuses:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = item.get("context") or item.get("name") or "unknown-context"
|
||||
value = (item.get("status") or item.get("state") or "unknown").lower()
|
||||
target = item.get("target_url") or item.get("url") or ""
|
||||
if value in pending_values:
|
||||
found = True
|
||||
if target:
|
||||
print(f"[ci-queue-wait] pending: {name}={value} ({target})")
|
||||
else:
|
||||
print(f"[ci-queue-wait] pending: {name}={value}")
|
||||
if not found:
|
||||
print("[ci-queue-wait] no pending contexts")
|
||||
PY
|
||||
}
|
||||
|
||||
github_get_branch_head_sha() {
|
||||
local owner="$1"
|
||||
local repo="$2"
|
||||
local branch="$3"
|
||||
gh api "repos/${owner}/${repo}/branches/${branch}" --jq '.commit.sha'
|
||||
}
|
||||
|
||||
github_get_commit_status_json() {
|
||||
local owner="$1"
|
||||
local repo="$2"
|
||||
local sha="$3"
|
||||
gh api "repos/${owner}/${repo}/commits/${sha}/status"
|
||||
}
|
||||
|
||||
gitea_get_branch_head_sha() {
|
||||
local host="$1"
|
||||
local repo="$2"
|
||||
local branch="$3"
|
||||
local token="$4"
|
||||
local url="https://${host}/api/v1/repos/${repo}/branches/${branch}"
|
||||
curl -fsS -H "Authorization: token ${token}" "$url" | python3 -c '
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
commit = data.get("commit") or {}
|
||||
print((commit.get("id") or "").strip())
|
||||
'
|
||||
}
|
||||
|
||||
gitea_get_commit_status_json() {
|
||||
local host="$1"
|
||||
local repo="$2"
|
||||
local sha="$3"
|
||||
local token="$4"
|
||||
local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status"
|
||||
curl -fsS -H "Authorization: token ${token}" "$url"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-B|--branch)
|
||||
BRANCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--timeout)
|
||||
TIMEOUT_SEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--interval)
|
||||
INTERVAL_SEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
--purpose)
|
||||
PURPOSE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--require-status)
|
||||
REQUIRE_STATUS=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! [[ "$TIMEOUT_SEC" =~ ^[0-9]+$ ]] || ! [[ "$INTERVAL_SEC" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: timeout and interval must be integer seconds." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OWNER=$(get_repo_owner)
|
||||
REPO=$(get_repo_name)
|
||||
detect_platform > /dev/null
|
||||
PLATFORM="${PLATFORM:-unknown}"
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "Error: gh CLI is required for GitHub CI queue guard." >&2
|
||||
exit 1
|
||||
fi
|
||||
HEAD_SHA=$(github_get_branch_head_sha "$OWNER" "$REPO" "$BRANCH")
|
||||
if [[ -z "$HEAD_SHA" ]]; then
|
||||
echo "Error: Could not resolve ${BRANCH} head SHA." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ci-queue-wait] platform=github purpose=${PURPOSE} branch=${BRANCH} sha=${HEAD_SHA}"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
HOST=$(get_remote_host) || {
|
||||
echo "Error: Could not determine remote host." >&2
|
||||
exit 1
|
||||
}
|
||||
TOKEN=$(get_gitea_token "$HOST") || {
|
||||
echo "Error: Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials." >&2
|
||||
exit 1
|
||||
}
|
||||
HEAD_SHA=$(gitea_get_branch_head_sha "$HOST" "$OWNER/$REPO" "$BRANCH" "$TOKEN")
|
||||
if [[ -z "$HEAD_SHA" ]]; then
|
||||
echo "Error: Could not resolve ${BRANCH} head SHA." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ci-queue-wait] platform=gitea purpose=${PURPOSE} branch=${BRANCH} sha=${HEAD_SHA}"
|
||||
else
|
||||
echo "Error: Unsupported platform '${PLATFORM}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
START_TS=$(date +%s)
|
||||
DEADLINE_TS=$((START_TS + TIMEOUT_SEC))
|
||||
|
||||
while true; do
|
||||
NOW_TS=$(date +%s)
|
||||
if (( NOW_TS > DEADLINE_TS )); then
|
||||
echo "Error: Timed out waiting for CI queue to clear on ${BRANCH} after ${TIMEOUT_SEC}s." >&2
|
||||
exit 124
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
STATUS_JSON=$(github_get_commit_status_json "$OWNER" "$REPO" "$HEAD_SHA")
|
||||
else
|
||||
STATUS_JSON=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$HEAD_SHA" "$TOKEN")
|
||||
fi
|
||||
|
||||
STATE=$(printf '%s' "$STATUS_JSON" | get_state_from_status_json)
|
||||
echo "[ci-queue-wait] state=${STATE} purpose=${PURPOSE} branch=${BRANCH}"
|
||||
|
||||
case "$STATE" in
|
||||
pending)
|
||||
printf '%s' "$STATUS_JSON" | print_pending_contexts
|
||||
sleep "$INTERVAL_SEC"
|
||||
;;
|
||||
no-status)
|
||||
if [[ "$REQUIRE_STATUS" -eq 1 ]]; then
|
||||
echo "Error: No CI status contexts found for ${BRANCH} while --require-status is set." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ci-queue-wait] no status contexts present; proceeding."
|
||||
exit 0
|
||||
;;
|
||||
terminal-success|terminal-failure|unknown)
|
||||
# Queue guard only blocks on pending/running/queued states.
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ci-queue-wait] unrecognized state '${STATE}', proceeding conservatively."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
83
tools/git/detect-platform.ps1
Normal file
83
tools/git/detect-platform.ps1
Normal file
@@ -0,0 +1,83 @@
|
||||
# detect-platform.ps1 - Detect git platform (Gitea or GitHub) for current repo
|
||||
# Usage: . .\detect-platform.ps1; Get-GitPlatform
|
||||
# or: .\detect-platform.ps1 (prints platform name)
|
||||
|
||||
function Get-GitPlatform {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$remoteUrl = git remote get-url origin 2>$null
|
||||
|
||||
if ([string]::IsNullOrEmpty($remoteUrl)) {
|
||||
Write-Error "Not a git repository or no origin remote"
|
||||
return $null
|
||||
}
|
||||
|
||||
# Check for GitHub
|
||||
if ($remoteUrl -match "github\.com") {
|
||||
return "github"
|
||||
}
|
||||
|
||||
# Check for common Gitea indicators
|
||||
# Gitea URLs typically don't contain github.com, gitlab.com, bitbucket.org
|
||||
if ($remoteUrl -notmatch "gitlab\.com" -and $remoteUrl -notmatch "bitbucket\.org") {
|
||||
# Assume Gitea for self-hosted repos
|
||||
return "gitea"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
function Get-GitRepoInfo {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$remoteUrl = git remote get-url origin 2>$null
|
||||
|
||||
if ([string]::IsNullOrEmpty($remoteUrl)) {
|
||||
Write-Error "Not a git repository or no origin remote"
|
||||
return $null
|
||||
}
|
||||
|
||||
# Extract owner/repo from URL
|
||||
# Handles: git@host:owner/repo.git, https://host/owner/repo.git, https://host/owner/repo
|
||||
$repoPath = $remoteUrl
|
||||
if ($remoteUrl -match "^git@") {
|
||||
$repoPath = ($remoteUrl -split ":")[1]
|
||||
} else {
|
||||
# Remove protocol and host
|
||||
$repoPath = $remoteUrl -replace "^https?://[^/]+/", ""
|
||||
}
|
||||
|
||||
# Remove .git suffix if present
|
||||
$repoPath = $repoPath -replace "\.git$", ""
|
||||
|
||||
return $repoPath
|
||||
}
|
||||
|
||||
function Get-GitRepoOwner {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$repoInfo = Get-GitRepoInfo
|
||||
if ($repoInfo) {
|
||||
return ($repoInfo -split "/")[0]
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-GitRepoName {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$repoInfo = Get-GitRepoInfo
|
||||
if ($repoInfo) {
|
||||
return ($repoInfo -split "/")[-1]
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# If script is run directly (not dot-sourced), output the platform
|
||||
if ($MyInvocation.InvocationName -ne ".") {
|
||||
Get-GitPlatform
|
||||
}
|
||||
80
tools/git/detect-platform.sh
Executable file
80
tools/git/detect-platform.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# detect-platform.sh - Detect git platform (Gitea or GitHub) for current repo
|
||||
# Usage: source detect-platform.sh && detect_platform
|
||||
# or: ./detect-platform.sh (prints platform name)
|
||||
|
||||
detect_platform() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
echo "error: not a git repository or no origin remote" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for GitHub
|
||||
if [[ "$remote_url" == *"github.com"* ]]; then
|
||||
PLATFORM="github"
|
||||
export PLATFORM
|
||||
echo "github"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for common Gitea indicators
|
||||
# Gitea URLs typically don't contain github.com, gitlab.com, bitbucket.org
|
||||
if [[ "$remote_url" != *"gitlab.com"* ]] && \
|
||||
[[ "$remote_url" != *"bitbucket.org"* ]]; then
|
||||
# Assume Gitea for self-hosted repos
|
||||
PLATFORM="gitea"
|
||||
export PLATFORM
|
||||
echo "gitea"
|
||||
return 0
|
||||
fi
|
||||
|
||||
PLATFORM="unknown"
|
||||
export PLATFORM
|
||||
echo "unknown"
|
||||
return 1
|
||||
}
|
||||
|
||||
get_repo_info() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
echo "error: not a git repository or no origin remote" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract owner/repo from URL
|
||||
# Handles: git@host:owner/repo.git, https://host/owner/repo.git, https://host/owner/repo
|
||||
local repo_path
|
||||
if [[ "$remote_url" == git@* ]]; then
|
||||
repo_path="${remote_url#*:}"
|
||||
else
|
||||
repo_path="${remote_url#*://}"
|
||||
repo_path="${repo_path#*/}"
|
||||
fi
|
||||
|
||||
# Remove .git suffix if present
|
||||
repo_path="${repo_path%.git}"
|
||||
|
||||
echo "$repo_path"
|
||||
}
|
||||
|
||||
get_repo_owner() {
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
echo "${repo_info%%/*}"
|
||||
}
|
||||
|
||||
get_repo_name() {
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
echo "${repo_info##*/}"
|
||||
}
|
||||
|
||||
# If script is run directly (not sourced), output the platform
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
detect_platform
|
||||
fi
|
||||
111
tools/git/issue-assign.ps1
Normal file
111
tools/git/issue-assign.ps1
Normal file
@@ -0,0 +1,111 @@
|
||||
# issue-assign.ps1 - Assign issues on Gitea or GitHub
|
||||
# Usage: .\issue-assign.ps1 -Issue ISSUE_NUMBER [-Assignee assignee] [-Labels labels] [-Milestone milestone]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Alias("i")]
|
||||
[int]$Issue,
|
||||
|
||||
[Alias("a")]
|
||||
[string]$Assignee,
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Labels,
|
||||
|
||||
[Alias("m")]
|
||||
[string]$Milestone,
|
||||
|
||||
[Alias("r")]
|
||||
[switch]$RemoveAssignee,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: issue-assign.ps1 [OPTIONS]
|
||||
|
||||
Assign or update an issue on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-Issue, -i NUMBER Issue number (required)
|
||||
-Assignee, -a USER Assign to user (use @me for self)
|
||||
-Labels, -l LABELS Add comma-separated labels
|
||||
-Milestone, -m NAME Set milestone
|
||||
-RemoveAssignee, -r Remove current assignee
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\issue-assign.ps1 -i 42 -a "username"
|
||||
.\issue-assign.ps1 -i 42 -l "in-progress" -m "0.2.0"
|
||||
.\issue-assign.ps1 -i 42 -a @me
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
if ($Assignee) {
|
||||
gh issue edit $Issue --add-assignee $Assignee
|
||||
}
|
||||
if ($RemoveAssignee) {
|
||||
$current = gh issue view $Issue --json assignees -q '.assignees[].login' 2>$null
|
||||
if ($current) {
|
||||
$assignees = ($current -split "`n") -join ","
|
||||
gh issue edit $Issue --remove-assignee $assignees
|
||||
}
|
||||
}
|
||||
if ($Labels) {
|
||||
gh issue edit $Issue --add-label $Labels
|
||||
}
|
||||
if ($Milestone) {
|
||||
gh issue edit $Issue --milestone $Milestone
|
||||
}
|
||||
Write-Host "Issue #$Issue updated successfully"
|
||||
}
|
||||
"gitea" {
|
||||
$needsEdit = $false
|
||||
$cmd = @("tea", "issue", "edit", $Issue)
|
||||
|
||||
if ($Assignee) {
|
||||
$cmd += @("--assignees", $Assignee)
|
||||
$needsEdit = $true
|
||||
}
|
||||
if ($Labels) {
|
||||
$cmd += @("--labels", $Labels)
|
||||
$needsEdit = $true
|
||||
}
|
||||
if ($Milestone) {
|
||||
$milestoneList = tea milestones list 2>$null
|
||||
$milestoneId = ($milestoneList | Select-String "^\s*(\d+).*$Milestone" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
|
||||
if ($milestoneId) {
|
||||
$cmd += @("--milestone", $milestoneId)
|
||||
$needsEdit = $true
|
||||
} else {
|
||||
Write-Warning "Could not find milestone '$Milestone'"
|
||||
}
|
||||
}
|
||||
|
||||
if ($needsEdit) {
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
Write-Host "Issue #$Issue updated successfully"
|
||||
} else {
|
||||
Write-Host "No changes specified"
|
||||
}
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
135
tools/git/issue-assign.sh
Executable file
135
tools/git/issue-assign.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
# issue-assign.sh - Assign issues on Gitea or GitHub
|
||||
# Usage: issue-assign.sh -i ISSUE_NUMBER [-a assignee] [-l labels] [-m milestone]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
ISSUE=""
|
||||
ASSIGNEE=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
REMOVE_ASSIGNEE=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Assign or update an issue on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-i, --issue NUMBER Issue number (required)
|
||||
-a, --assignee USER Assign to user (use @me for self)
|
||||
-l, --labels LABELS Add comma-separated labels
|
||||
-m, --milestone NAME Set milestone
|
||||
-r, --remove-assignee Remove current assignee
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -i 42 -a "username"
|
||||
$(basename "$0") -i 42 -l "in-progress" -m "0.2.0"
|
||||
$(basename "$0") -i 42 -a @me
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--assignee)
|
||||
ASSIGNEE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--labels)
|
||||
LABELS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--remove-assignee)
|
||||
REMOVE_ASSIGNEE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE" ]]; then
|
||||
echo "Error: Issue number is required (-i)" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
if [[ -n "$ASSIGNEE" ]]; then
|
||||
gh issue edit "$ISSUE" --add-assignee "$ASSIGNEE"
|
||||
fi
|
||||
if [[ "$REMOVE_ASSIGNEE" == true ]]; then
|
||||
# Get current assignees and remove them
|
||||
CURRENT=$(gh issue view "$ISSUE" --json assignees -q '.assignees[].login' 2>/dev/null | tr '\n' ',')
|
||||
if [[ -n "$CURRENT" ]]; then
|
||||
gh issue edit "$ISSUE" --remove-assignee "${CURRENT%,}"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$LABELS" ]]; then
|
||||
gh issue edit "$ISSUE" --add-label "$LABELS"
|
||||
fi
|
||||
if [[ -n "$MILESTONE" ]]; then
|
||||
gh issue edit "$ISSUE" --milestone "$MILESTONE"
|
||||
fi
|
||||
echo "Issue #$ISSUE updated successfully"
|
||||
;;
|
||||
gitea)
|
||||
# tea issue edit syntax
|
||||
CMD="tea issue edit $ISSUE"
|
||||
NEEDS_EDIT=false
|
||||
|
||||
if [[ -n "$ASSIGNEE" ]]; then
|
||||
# tea uses --assignees flag
|
||||
CMD="$CMD --assignees \"$ASSIGNEE\""
|
||||
NEEDS_EDIT=true
|
||||
fi
|
||||
if [[ -n "$LABELS" ]]; then
|
||||
# tea uses --labels flag (replaces existing)
|
||||
CMD="$CMD --labels \"$LABELS\""
|
||||
NEEDS_EDIT=true
|
||||
fi
|
||||
if [[ -n "$MILESTONE" ]]; then
|
||||
MILESTONE_ID=$(tea milestones list 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1)
|
||||
if [[ -n "$MILESTONE_ID" ]]; then
|
||||
CMD="$CMD --milestone $MILESTONE_ID"
|
||||
NEEDS_EDIT=true
|
||||
else
|
||||
echo "Warning: Could not find milestone '$MILESTONE'" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$NEEDS_EDIT" == true ]]; then
|
||||
eval "$CMD"
|
||||
echo "Issue #$ISSUE updated successfully"
|
||||
else
|
||||
echo "No changes specified"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
64
tools/git/issue-close.sh
Executable file
64
tools/git/issue-close.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# issue-close.sh - Close an issue on GitHub or Gitea
|
||||
# Usage: issue-close.sh -i <issue_number> [-c <comment>]
|
||||
|
||||
set -e
|
||||
|
||||
# Source platform detection
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-close.sh -i <issue_number> [-c <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -c, --comment Comment to add before closing (optional)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect platform and close issue
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||
fi
|
||||
gh issue close "$ISSUE_NUMBER"
|
||||
echo "Closed GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
tea issue comment "$ISSUE_NUMBER" "$COMMENT"
|
||||
fi
|
||||
tea issue close "$ISSUE_NUMBER"
|
||||
echo "Closed Gitea issue #$ISSUE_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
61
tools/git/issue-comment.sh
Executable file
61
tools/git/issue-comment.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# issue-comment.sh - Add a comment to an issue on GitHub or Gitea
|
||||
# Usage: issue-comment.sh -i <issue_number> -c <comment>
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-comment.sh -i <issue_number> -c <comment>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -c, --comment Comment text (required)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment is required (-c)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||
echo "Added comment to GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea issue comment "$ISSUE_NUMBER" "$COMMENT"
|
||||
echo "Added comment to Gitea issue #$ISSUE_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
80
tools/git/issue-create.ps1
Normal file
80
tools/git/issue-create.ps1
Normal file
@@ -0,0 +1,80 @@
|
||||
# issue-create.ps1 - Create issues on Gitea or GitHub
|
||||
# Usage: .\issue-create.ps1 -Title "Title" [-Body "Body"] [-Labels "label1,label2"] [-Milestone "milestone"]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Alias("t")]
|
||||
[string]$Title,
|
||||
|
||||
[Alias("b")]
|
||||
[string]$Body,
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Labels,
|
||||
|
||||
[Alias("m")]
|
||||
[string]$Milestone,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: issue-create.ps1 [OPTIONS]
|
||||
|
||||
Create an issue on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-Title, -t TITLE Issue title (required)
|
||||
-Body, -b BODY Issue body/description
|
||||
-Labels, -l LABELS Comma-separated labels (e.g., "bug,feature")
|
||||
-Milestone, -m NAME Milestone name to assign
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\issue-create.ps1 -Title "Fix login bug" -Labels "bug,priority-high"
|
||||
.\issue-create.ps1 -t "Add dark mode" -b "Implement theme switching" -m "0.2.0"
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "issue", "create", "--title", $Title)
|
||||
if ($Body) { $cmd += @("--body", $Body) }
|
||||
if ($Labels) { $cmd += @("--label", $Labels) }
|
||||
if ($Milestone) { $cmd += @("--milestone", $Milestone) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "issue", "create", "--title", $Title)
|
||||
if ($Body) { $cmd += @("--description", $Body) }
|
||||
if ($Labels) { $cmd += @("--labels", $Labels) }
|
||||
if ($Milestone) {
|
||||
# Try to get milestone ID by name
|
||||
$milestoneList = tea milestones list 2>$null
|
||||
$milestoneId = ($milestoneList | Select-String "^\s*(\d+).*$Milestone" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
|
||||
if ($milestoneId) {
|
||||
$cmd += @("--milestone", $milestoneId)
|
||||
} else {
|
||||
Write-Warning "Could not find milestone '$Milestone', creating without milestone"
|
||||
}
|
||||
}
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
172
tools/git/issue-create.sh
Executable file
172
tools/git/issue-create.sh
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
# issue-create.sh - Create issues on Gitea or GitHub
|
||||
# Usage: issue-create.sh -t "Title" [-b "Body"] [-l "label1,label2"] [-m "milestone"]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
TITLE=""
|
||||
BODY=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
gitea_issue_create_api() {
|
||||
local host repo token url payload
|
||||
host=$(get_remote_host) || {
|
||||
echo "Error: could not determine remote host for API fallback" >&2
|
||||
return 1
|
||||
}
|
||||
repo=$(get_repo_info) || {
|
||||
echo "Error: could not determine repo owner/name for API fallback" >&2
|
||||
return 1
|
||||
}
|
||||
token=$(get_gitea_token "$host") || {
|
||||
echo "Error: Gitea token not found for API fallback (set GITEA_TOKEN or configure ~/.git-credentials)" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -n "$LABELS" || -n "$MILESTONE" ]]; then
|
||||
echo "Warning: API fallback currently applies title/body only; labels/milestone require authenticated tea setup." >&2
|
||||
fi
|
||||
|
||||
payload=$(TITLE="$TITLE" BODY="$BODY" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
payload = {"title": os.environ["TITLE"]}
|
||||
body = os.environ.get("BODY", "")
|
||||
if body:
|
||||
payload["body"] = body
|
||||
print(json.dumps(payload))
|
||||
PY
|
||||
)
|
||||
|
||||
url="https://${host}/api/v1/repos/${repo}/issues"
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token ${token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$url"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Create an issue on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-t, --title TITLE Issue title (required)
|
||||
-b, --body BODY Issue body/description
|
||||
-l, --labels LABELS Comma-separated labels (e.g., "bug,feature")
|
||||
-m, --milestone NAME Milestone name to assign
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -t "Fix login bug" -l "bug,priority-high"
|
||||
$(basename "$0") -t "Add dark mode" -b "Implement theme switching" -m "0.2.0"
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--body)
|
||||
BODY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--labels)
|
||||
LABELS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TITLE" ]]; then
|
||||
echo "Error: Title is required (-t)" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh issue create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --body \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --label \"$LABELS\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
if command -v tea >/dev/null 2>&1; then
|
||||
CMD="tea issue create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --labels \"$LABELS\""
|
||||
# tea accepts milestone by name directly (verified 2026-02-05)
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
if eval "$CMD"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Warning: tea issue create failed, trying Gitea API fallback..." >&2
|
||||
fi
|
||||
gitea_issue_create_api
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
84
tools/git/issue-edit.sh
Executable file
84
tools/git/issue-edit.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# issue-edit.sh - Edit an issue on GitHub or Gitea
|
||||
# Usage: issue-edit.sh -i <issue_number> [-t <title>] [-b <body>] [-l <labels>] [-m <milestone>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
TITLE=""
|
||||
BODY=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--body)
|
||||
BODY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--labels)
|
||||
LABELS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-edit.sh -i <issue_number> [-t <title>] [-b <body>] [-l <labels>] [-m <milestone>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -t, --title New title"
|
||||
echo " -b, --body New body/description"
|
||||
echo " -l, --labels Labels (comma-separated, replaces existing)"
|
||||
echo " -m, --milestone Milestone name"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
CMD="gh issue edit $ISSUE_NUMBER"
|
||||
[[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --body \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --add-label \"$LABELS\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
eval $CMD
|
||||
echo "Updated GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
CMD="tea issue edit $ISSUE_NUMBER"
|
||||
[[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --add-labels \"$LABELS\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
eval $CMD
|
||||
echo "Updated Gitea issue #$ISSUE_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
78
tools/git/issue-list.ps1
Normal file
78
tools/git/issue-list.ps1
Normal file
@@ -0,0 +1,78 @@
|
||||
# issue-list.ps1 - List issues on Gitea or GitHub
|
||||
# Usage: .\issue-list.ps1 [-State state] [-Label label] [-Milestone milestone] [-Assignee assignee]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("s")]
|
||||
[ValidateSet("open", "closed", "all")]
|
||||
[string]$State = "open",
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Label,
|
||||
|
||||
[Alias("m")]
|
||||
[string]$Milestone,
|
||||
|
||||
[Alias("a")]
|
||||
[string]$Assignee,
|
||||
|
||||
[Alias("n")]
|
||||
[int]$Limit = 100,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: issue-list.ps1 [OPTIONS]
|
||||
|
||||
List issues from the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-State, -s STATE Filter by state: open, closed, all (default: open)
|
||||
-Label, -l LABEL Filter by label
|
||||
-Milestone, -m NAME Filter by milestone name
|
||||
-Assignee, -a USER Filter by assignee
|
||||
-Limit, -n N Maximum issues to show (default: 100)
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\issue-list.ps1 # List open issues
|
||||
.\issue-list.ps1 -s all -l bug # All issues with 'bug' label
|
||||
.\issue-list.ps1 -m "0.2.0" # Issues in milestone 0.2.0
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "issue", "list", "--state", $State, "--limit", $Limit)
|
||||
if ($Label) { $cmd += @("--label", $Label) }
|
||||
if ($Milestone) { $cmd += @("--milestone", $Milestone) }
|
||||
if ($Assignee) { $cmd += @("--assignee", $Assignee) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "issues", "list", "--state", $State, "--limit", $Limit)
|
||||
if ($Label) { $cmd += @("--labels", $Label) }
|
||||
if ($Milestone) { $cmd += @("--milestones", $Milestone) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
if ($Assignee) {
|
||||
Write-Warning "Assignee filtering may require manual review for Gitea"
|
||||
}
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
96
tools/git/issue-list.sh
Executable file
96
tools/git/issue-list.sh
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
# issue-list.sh - List issues on Gitea or GitHub
|
||||
# Usage: issue-list.sh [-s state] [-l label] [-m milestone] [-a assignee]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
STATE="open"
|
||||
LABEL=""
|
||||
MILESTONE=""
|
||||
ASSIGNEE=""
|
||||
LIMIT=100
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
List issues from the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-s, --state STATE Filter by state: open, closed, all (default: open)
|
||||
-l, --label LABEL Filter by label
|
||||
-m, --milestone NAME Filter by milestone name
|
||||
-a, --assignee USER Filter by assignee
|
||||
-n, --limit N Maximum issues to show (default: 100)
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # List open issues
|
||||
$(basename "$0") -s all -l bug # All issues with 'bug' label
|
||||
$(basename "$0") -m "0.2.0" # Issues in milestone 0.2.0
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--state)
|
||||
STATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--label)
|
||||
LABEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--assignee)
|
||||
ASSIGNEE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-n|--limit)
|
||||
LIMIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh issue list --state $STATE --limit $LIMIT"
|
||||
[[ -n "$LABEL" ]] && CMD="$CMD --label \"$LABEL\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
[[ -n "$ASSIGNEE" ]] && CMD="$CMD --assignee \"$ASSIGNEE\""
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
CMD="tea issues list --state $STATE --limit $LIMIT"
|
||||
[[ -n "$LABEL" ]] && CMD="$CMD --labels \"$LABEL\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestones \"$MILESTONE\""
|
||||
# Note: tea may not support assignee filter directly
|
||||
eval "$CMD"
|
||||
if [[ -n "$ASSIGNEE" ]]; then
|
||||
echo "Note: Assignee filtering may require manual review for Gitea" >&2
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
62
tools/git/issue-reopen.sh
Executable file
62
tools/git/issue-reopen.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# issue-reopen.sh - Reopen a closed issue on GitHub or Gitea
|
||||
# Usage: issue-reopen.sh -i <issue_number> [-c <comment>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-reopen.sh -i <issue_number> [-c <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -c, --comment Comment to add when reopening (optional)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||
fi
|
||||
gh issue reopen "$ISSUE_NUMBER"
|
||||
echo "Reopened GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
tea issue comment "$ISSUE_NUMBER" "$COMMENT"
|
||||
fi
|
||||
tea issue reopen "$ISSUE_NUMBER"
|
||||
echo "Reopened Gitea issue #$ISSUE_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
112
tools/git/issue-view.sh
Executable file
112
tools/git/issue-view.sh
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# issue-view.sh - View issue details on GitHub or Gitea
|
||||
# Usage: issue-view.sh -i <issue_number>
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
gitea_issue_view_api() {
|
||||
local host repo token url
|
||||
host=$(get_remote_host) || {
|
||||
echo "Error: could not determine remote host for API fallback" >&2
|
||||
return 1
|
||||
}
|
||||
repo=$(get_repo_info) || {
|
||||
echo "Error: could not determine repo owner/name for API fallback" >&2
|
||||
return 1
|
||||
}
|
||||
token=$(get_gitea_token "$host") || {
|
||||
echo "Error: Gitea token not found for API fallback (set GITEA_TOKEN or configure ~/.git-credentials)" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
url="https://${host}/api/v1/repos/${repo}/issues/${ISSUE_NUMBER}"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
curl -fsS -H "Authorization: token ${token}" "$url" | python3 -m json.tool
|
||||
else
|
||||
curl -fsS -H "Authorization: token ${token}" "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-view.sh -i <issue_number>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh issue view "$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
if command -v tea >/dev/null 2>&1; then
|
||||
if tea issue "$ISSUE_NUMBER"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Warning: tea issue view failed, trying Gitea API fallback..." >&2
|
||||
fi
|
||||
gitea_issue_view_api
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
50
tools/git/milestone-close.sh
Executable file
50
tools/git/milestone-close.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# milestone-close.sh - Close a milestone on GitHub or Gitea
|
||||
# Usage: milestone-close.sh -t <title>
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
TITLE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: milestone-close.sh -t <title>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -t, --title Milestone title (required)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TITLE" ]]; then
|
||||
echo "Error: Milestone title is required (-t)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh api -X PATCH "/repos/{owner}/{repo}/milestones/$(gh api "/repos/{owner}/{repo}/milestones" --jq ".[] | select(.title==\"$TITLE\") | .number")" -f state=closed
|
||||
echo "Closed GitHub milestone: $TITLE"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea milestone close "$TITLE"
|
||||
echo "Closed Gitea milestone: $TITLE"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
98
tools/git/milestone-create.ps1
Normal file
98
tools/git/milestone-create.ps1
Normal file
@@ -0,0 +1,98 @@
|
||||
# milestone-create.ps1 - Create milestones on Gitea or GitHub
|
||||
# Usage: .\milestone-create.ps1 -Title "Title" [-Description "Description"] [-Due "YYYY-MM-DD"]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("t")]
|
||||
[string]$Title,
|
||||
|
||||
[Alias("d")]
|
||||
[string]$Description,
|
||||
|
||||
[string]$Due,
|
||||
|
||||
[switch]$List,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: milestone-create.ps1 [OPTIONS]
|
||||
|
||||
Create or list milestones on the current repository (Gitea or GitHub).
|
||||
|
||||
Versioning Convention:
|
||||
- Features get dedicated milestones
|
||||
- Pre-MVP milestones MUST use 0.0.x and MUST start at 0.0.1
|
||||
- 0.1.0 is reserved for MVP release
|
||||
- After MVP, continue semantic progression (0.1.x, 0.2.x, ...)
|
||||
|
||||
Options:
|
||||
-Title, -t TITLE Milestone title/version (e.g., "0.0.1")
|
||||
-Description, -d DESC Milestone description
|
||||
-Due DATE Due date (YYYY-MM-DD format)
|
||||
-List List existing milestones
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\milestone-create.ps1 -List
|
||||
.\milestone-create.ps1 -t "0.0.1" -d "Pre-MVP Foundation Sprint"
|
||||
.\milestone-create.ps1 -t "0.1.0" -d "MVP Release" -Due "2025-03-01"
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
if ($List) {
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
gh api repos/:owner/:repo/milestones --jq '.[] | "\(.number)`t\(.title)`t\(.state)`t\(.open_issues)/\(.closed_issues) issues"'
|
||||
}
|
||||
"gitea" {
|
||||
tea milestones list
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $Title) {
|
||||
Write-Error "Title is required (-t) for creating milestones"
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$payload = @{ title = $Title }
|
||||
if ($Description) { $payload.description = $Description }
|
||||
if ($Due) { $payload.due_on = "${Due}T00:00:00Z" }
|
||||
|
||||
$json = $payload | ConvertTo-Json -Compress
|
||||
$json | gh api repos/:owner/:repo/milestones --method POST --input -
|
||||
Write-Host "Milestone '$Title' created successfully"
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "milestones", "create", "--title", $Title)
|
||||
if ($Description) { $cmd += @("--description", $Description) }
|
||||
if ($Due) { $cmd += @("--deadline", $Due) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
Write-Host "Milestone '$Title' created successfully"
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
117
tools/git/milestone-create.sh
Executable file
117
tools/git/milestone-create.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# milestone-create.sh - Create milestones on Gitea or GitHub
|
||||
# Usage: milestone-create.sh -t "Title" [-d "Description"] [--due "YYYY-MM-DD"]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
TITLE=""
|
||||
DESCRIPTION=""
|
||||
DUE_DATE=""
|
||||
LIST_ONLY=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Create or list milestones on the current repository (Gitea or GitHub).
|
||||
|
||||
Versioning Convention:
|
||||
- Features get dedicated milestones
|
||||
- Pre-MVP milestones MUST use 0.0.x and MUST start at 0.0.1
|
||||
- 0.1.0 is reserved for MVP release
|
||||
- After MVP, continue semantic progression (0.1.x, 0.2.x, ...)
|
||||
|
||||
Options:
|
||||
-t, --title TITLE Milestone title/version (e.g., "0.0.1")
|
||||
-d, --desc DESCRIPTION Milestone description
|
||||
--due DATE Due date (YYYY-MM-DD format)
|
||||
--list List existing milestones
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --list
|
||||
$(basename "$0") -t "0.0.1" -d "Pre-MVP Foundation Sprint"
|
||||
$(basename "$0") -t "0.1.0" -d "MVP Release" --due "2025-03-01"
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--desc)
|
||||
DESCRIPTION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--due)
|
||||
DUE_DATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--list)
|
||||
LIST_ONLY=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
if [[ "$LIST_ONLY" == true ]]; then
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
gh api repos/:owner/:repo/milestones --jq '.[] | "\(.number)\t\(.title)\t\(.state)\t\(.open_issues)/\(.closed_issues) issues"'
|
||||
;;
|
||||
gitea)
|
||||
tea milestones list
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$TITLE" ]]; then
|
||||
echo "Error: Title is required (-t) for creating milestones" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
# GitHub uses the API for milestone creation
|
||||
JSON_PAYLOAD="{\"title\":\"$TITLE\""
|
||||
[[ -n "$DESCRIPTION" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"description\":\"$DESCRIPTION\""
|
||||
[[ -n "$DUE_DATE" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"due_on\":\"${DUE_DATE}T00:00:00Z\""
|
||||
JSON_PAYLOAD="$JSON_PAYLOAD}"
|
||||
|
||||
gh api repos/:owner/:repo/milestones --method POST --input - <<< "$JSON_PAYLOAD"
|
||||
echo "Milestone '$TITLE' created successfully"
|
||||
;;
|
||||
gitea)
|
||||
CMD="tea milestones create --title \"$TITLE\""
|
||||
[[ -n "$DESCRIPTION" ]] && CMD="$CMD --description \"$DESCRIPTION\""
|
||||
[[ -n "$DUE_DATE" ]] && CMD="$CMD --deadline \"$DUE_DATE\""
|
||||
eval "$CMD"
|
||||
echo "Milestone '$TITLE' created successfully"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
43
tools/git/milestone-list.sh
Executable file
43
tools/git/milestone-list.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# milestone-list.sh - List milestones on GitHub or Gitea
|
||||
# Usage: milestone-list.sh [-s <state>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
STATE="open"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--state)
|
||||
STATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: milestone-list.sh [-s <state>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -s, --state Filter by state: open, closed, all (default: open)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh api "/repos/{owner}/{repo}/milestones?state=$STATE" --jq '.[] | "\(.title) (\(.state)) - \(.open_issues) open, \(.closed_issues) closed"'
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea milestone list
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
273
tools/git/pr-ci-wait.sh
Executable file
273
tools/git/pr-ci-wait.sh
Executable file
@@ -0,0 +1,273 @@
|
||||
#!/bin/bash
|
||||
# pr-ci-wait.sh - Wait for PR CI status to reach terminal state (GitHub/Gitea)
|
||||
# Usage: pr-ci-wait.sh -n <pr_number> [-t timeout_sec] [-i interval_sec]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
PR_NUMBER=""
|
||||
TIMEOUT_SEC=1800
|
||||
INTERVAL_SEC=15
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") -n <pr_number> [-t timeout_sec] [-i interval_sec]
|
||||
|
||||
Options:
|
||||
-n, --number NUMBER PR number (required)
|
||||
-t, --timeout SECONDS Max wait time in seconds (default: 1800)
|
||||
-i, --interval SECONDS Poll interval in seconds (default: 15)
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -n 643
|
||||
$(basename "$0") -n 643 -t 900 -i 10
|
||||
EOF
|
||||
}
|
||||
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_state_from_status_json() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print("unknown")
|
||||
raise SystemExit(0)
|
||||
|
||||
state = (payload.get("state") or "").lower()
|
||||
if state in {"success", "pending", "failure", "error"}:
|
||||
print(state)
|
||||
raise SystemExit(0)
|
||||
|
||||
statuses = payload.get("statuses") or []
|
||||
values = []
|
||||
for item in statuses:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
value = (item.get("status") or item.get("state") or "").lower()
|
||||
if value:
|
||||
values.append(value)
|
||||
|
||||
if any(v in {"failure", "error"} for v in values):
|
||||
print("failure")
|
||||
elif values and all(v == "success" for v in values):
|
||||
print("success")
|
||||
elif any(v in {"pending", "running", "queued", "waiting"} for v in values):
|
||||
print("pending")
|
||||
else:
|
||||
print("unknown")
|
||||
PY
|
||||
}
|
||||
|
||||
print_status_summary() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print("[pr-ci-wait] status payload unavailable")
|
||||
raise SystemExit(0)
|
||||
|
||||
statuses = payload.get("statuses") or []
|
||||
if not statuses:
|
||||
print("[pr-ci-wait] no status contexts reported yet")
|
||||
raise SystemExit(0)
|
||||
|
||||
for item in statuses:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = item.get("context") or item.get("name") or "unknown-context"
|
||||
state = item.get("status") or item.get("state") or "unknown-state"
|
||||
target = item.get("target_url") or item.get("url") or ""
|
||||
if target:
|
||||
print(f"[pr-ci-wait] {name}: {state} ({target})")
|
||||
else:
|
||||
print(f"[pr-ci-wait] {name}: {state}")
|
||||
PY
|
||||
}
|
||||
|
||||
github_get_pr_head_sha() {
|
||||
gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid'
|
||||
}
|
||||
|
||||
github_get_commit_status_json() {
|
||||
local owner="$1"
|
||||
local repo="$2"
|
||||
local sha="$3"
|
||||
gh api "repos/${owner}/${repo}/commits/${sha}/status"
|
||||
}
|
||||
|
||||
gitea_get_pr_head_sha() {
|
||||
local host="$1"
|
||||
local repo="$2"
|
||||
local token="$3"
|
||||
local url="https://${host}/api/v1/repos/${repo}/pulls/${PR_NUMBER}"
|
||||
curl -fsS -H "Authorization: token ${token}" "$url" | python3 -c '
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
print((data.get("head") or {}).get("sha", ""))
|
||||
'
|
||||
}
|
||||
|
||||
gitea_get_commit_status_json() {
|
||||
local host="$1"
|
||||
local repo="$2"
|
||||
local token="$3"
|
||||
local sha="$4"
|
||||
local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status"
|
||||
curl -fsS -H "Authorization: token ${token}" "$url"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--timeout)
|
||||
TIMEOUT_SEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--interval)
|
||||
INTERVAL_SEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$TIMEOUT_SEC" =~ ^[0-9]+$ ]] || ! [[ "$INTERVAL_SEC" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: timeout and interval must be integer seconds." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform > /dev/null
|
||||
|
||||
OWNER=$(get_repo_owner)
|
||||
REPO=$(get_repo_name)
|
||||
START_TS=$(date +%s)
|
||||
DEADLINE_TS=$((START_TS + TIMEOUT_SEC))
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "Error: gh CLI is required for GitHub CI status polling." >&2
|
||||
exit 1
|
||||
fi
|
||||
HEAD_SHA=$(github_get_pr_head_sha)
|
||||
if [[ -z "$HEAD_SHA" ]]; then
|
||||
echo "Error: Could not resolve head SHA for PR #$PR_NUMBER." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[pr-ci-wait] Platform=github PR=#${PR_NUMBER} head_sha=${HEAD_SHA}"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
HOST=$(get_remote_host) || {
|
||||
echo "Error: Could not determine remote host." >&2
|
||||
exit 1
|
||||
}
|
||||
TOKEN=$(get_gitea_token "$HOST") || {
|
||||
echo "Error: Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials." >&2
|
||||
exit 1
|
||||
}
|
||||
HEAD_SHA=$(gitea_get_pr_head_sha "$HOST" "$OWNER/$REPO" "$TOKEN")
|
||||
if [[ -z "$HEAD_SHA" ]]; then
|
||||
echo "Error: Could not resolve head SHA for PR #$PR_NUMBER." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[pr-ci-wait] Platform=gitea host=${HOST} PR=#${PR_NUMBER} head_sha=${HEAD_SHA}"
|
||||
else
|
||||
echo "Error: Unsupported platform '${PLATFORM}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
NOW_TS=$(date +%s)
|
||||
if (( NOW_TS > DEADLINE_TS )); then
|
||||
echo "Error: Timed out waiting for CI status on PR #$PR_NUMBER after ${TIMEOUT_SEC}s." >&2
|
||||
exit 124
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
STATUS_JSON=$(github_get_commit_status_json "$OWNER" "$REPO" "$HEAD_SHA")
|
||||
else
|
||||
STATUS_JSON=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$TOKEN" "$HEAD_SHA")
|
||||
fi
|
||||
|
||||
STATE=$(printf '%s' "$STATUS_JSON" | extract_state_from_status_json)
|
||||
echo "[pr-ci-wait] state=${STATE} pr=#${PR_NUMBER} sha=${HEAD_SHA}"
|
||||
|
||||
case "$STATE" in
|
||||
success)
|
||||
printf '%s' "$STATUS_JSON" | print_status_summary
|
||||
echo "[pr-ci-wait] CI is green for PR #$PR_NUMBER."
|
||||
exit 0
|
||||
;;
|
||||
failure|error)
|
||||
printf '%s' "$STATUS_JSON" | print_status_summary
|
||||
echo "Error: CI reported ${STATE} for PR #$PR_NUMBER." >&2
|
||||
exit 1
|
||||
;;
|
||||
pending|unknown)
|
||||
sleep "$INTERVAL_SEC"
|
||||
;;
|
||||
*)
|
||||
echo "[pr-ci-wait] Unrecognized state '${STATE}', continuing to poll..."
|
||||
sleep "$INTERVAL_SEC"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
62
tools/git/pr-close.sh
Executable file
62
tools/git/pr-close.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# pr-close.sh - Close a pull request without merging on GitHub or Gitea
|
||||
# Usage: pr-close.sh -n <pr_number> [-c <comment>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-close.sh -n <pr_number> [-c <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -c, --comment Comment before closing (optional)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
gh pr comment "$PR_NUMBER" --body "$COMMENT"
|
||||
fi
|
||||
gh pr close "$PR_NUMBER"
|
||||
echo "Closed GitHub PR #$PR_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
tea pr comment "$PR_NUMBER" "$COMMENT"
|
||||
fi
|
||||
tea pr close "$PR_NUMBER"
|
||||
echo "Closed Gitea PR #$PR_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
130
tools/git/pr-create.ps1
Normal file
130
tools/git/pr-create.ps1
Normal file
@@ -0,0 +1,130 @@
|
||||
# pr-create.ps1 - Create pull requests on Gitea or GitHub
|
||||
# Usage: .\pr-create.ps1 -Title "Title" [-Body "Body"] [-Base base] [-Head head] [-Labels "labels"] [-Milestone "milestone"]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("t")]
|
||||
[string]$Title,
|
||||
|
||||
[Alias("b")]
|
||||
[string]$Body,
|
||||
|
||||
[Alias("B")]
|
||||
[string]$Base,
|
||||
|
||||
[Alias("H")]
|
||||
[string]$Head,
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Labels,
|
||||
|
||||
[Alias("m")]
|
||||
[string]$Milestone,
|
||||
|
||||
[Alias("i")]
|
||||
[int]$Issue,
|
||||
|
||||
[Alias("d")]
|
||||
[switch]$Draft,
|
||||
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: pr-create.ps1 [OPTIONS]
|
||||
|
||||
Create a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-Title, -t TITLE PR title (required, or use -Issue)
|
||||
-Body, -b BODY PR description/body
|
||||
-Base, -B BRANCH Base branch to merge into (default: main/master)
|
||||
-Head, -H BRANCH Head branch with changes (default: current branch)
|
||||
-Labels, -l LABELS Comma-separated labels
|
||||
-Milestone, -m NAME Milestone name
|
||||
-Issue, -i NUMBER Link to issue (auto-generates title if not provided)
|
||||
-Draft, -d Create as draft PR
|
||||
-Help Show this help message
|
||||
|
||||
Examples:
|
||||
.\pr-create.ps1 -Title "Add login feature" -Body "Implements user authentication"
|
||||
.\pr-create.ps1 -t "Fix bug" -B main -H feature/fix-123
|
||||
.\pr-create.ps1 -i 42 -b "Implements the feature described in #42"
|
||||
.\pr-create.ps1 -t "WIP: New feature" -Draft
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
# If no title but issue provided, generate title
|
||||
if (-not $Title -and $Issue) {
|
||||
$Title = "Fixes #$Issue"
|
||||
}
|
||||
|
||||
if (-not $Title) {
|
||||
Write-Error "Title is required (-t) or provide an issue (-i)"
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
# Default head branch to current branch
|
||||
if (-not $Head) {
|
||||
$Head = git branch --show-current
|
||||
}
|
||||
|
||||
# Add issue reference to body if provided
|
||||
if ($Issue) {
|
||||
if ($Body) {
|
||||
$Body = "$Body`n`nFixes #$Issue"
|
||||
} else {
|
||||
$Body = "Fixes #$Issue"
|
||||
}
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "pr", "create", "--title", $Title)
|
||||
if ($Body) { $cmd += @("--body", $Body) }
|
||||
if ($Base) { $cmd += @("--base", $Base) }
|
||||
if ($Head) { $cmd += @("--head", $Head) }
|
||||
if ($Labels) { $cmd += @("--label", $Labels) }
|
||||
if ($Milestone) { $cmd += @("--milestone", $Milestone) }
|
||||
if ($Draft) { $cmd += "--draft" }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "pr", "create", "--title", $Title)
|
||||
if ($Body) { $cmd += @("--description", $Body) }
|
||||
if ($Base) { $cmd += @("--base", $Base) }
|
||||
if ($Head) { $cmd += @("--head", $Head) }
|
||||
if ($Labels) { $cmd += @("--labels", $Labels) }
|
||||
|
||||
if ($Milestone) {
|
||||
$milestoneList = tea milestones list 2>$null
|
||||
$milestoneId = ($milestoneList | Select-String "^\s*(\d+).*$Milestone" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
|
||||
if ($milestoneId) {
|
||||
$cmd += @("--milestone", $milestoneId)
|
||||
} else {
|
||||
Write-Warning "Could not find milestone '$Milestone', creating without milestone"
|
||||
}
|
||||
}
|
||||
|
||||
if ($Draft) {
|
||||
Write-Warning "Draft PR may not be supported by your tea version"
|
||||
}
|
||||
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
164
tools/git/pr-create.sh
Executable file
164
tools/git/pr-create.sh
Executable file
@@ -0,0 +1,164 @@
|
||||
#!/bin/bash
|
||||
# pr-create.sh - Create pull requests on Gitea or GitHub
|
||||
# Usage: pr-create.sh -t "Title" [-b "Body"] [-B base] [-H head] [-l "labels"] [-m "milestone"]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
TITLE=""
|
||||
BODY=""
|
||||
BASE_BRANCH=""
|
||||
HEAD_BRANCH=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
DRAFT=false
|
||||
ISSUE=""
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Create a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-t, --title TITLE PR title (required, or use --issue)
|
||||
-b, --body BODY PR description/body
|
||||
-B, --base BRANCH Base branch to merge into (default: main/master)
|
||||
-H, --head BRANCH Head branch with changes (default: current branch)
|
||||
-l, --labels LABELS Comma-separated labels
|
||||
-m, --milestone NAME Milestone name
|
||||
-i, --issue NUMBER Link to issue (auto-generates title if not provided)
|
||||
-d, --draft Create as draft PR
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -t "Add login feature" -b "Implements user authentication"
|
||||
$(basename "$0") -t "Fix bug" -B main -H feature/fix-123
|
||||
$(basename "$0") -i 42 -b "Implements the feature described in #42"
|
||||
$(basename "$0") -t "WIP: New feature" --draft
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--body)
|
||||
BODY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-B|--base)
|
||||
BASE_BRANCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-H|--head)
|
||||
HEAD_BRANCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--labels)
|
||||
LABELS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--issue)
|
||||
ISSUE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--draft)
|
||||
DRAFT=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# If no title but issue provided, generate title
|
||||
if [[ -z "$TITLE" ]] && [[ -n "$ISSUE" ]]; then
|
||||
TITLE="Fixes #$ISSUE"
|
||||
fi
|
||||
|
||||
if [[ -z "$TITLE" ]]; then
|
||||
echo "Error: Title is required (-t) or provide an issue (-i)" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
# Default head branch to current branch
|
||||
if [[ -z "$HEAD_BRANCH" ]]; then
|
||||
HEAD_BRANCH=$(git branch --show-current)
|
||||
fi
|
||||
|
||||
# Add issue reference to body if provided
|
||||
if [[ -n "$ISSUE" ]]; then
|
||||
if [[ -n "$BODY" ]]; then
|
||||
BODY="$BODY
|
||||
|
||||
Fixes #$ISSUE"
|
||||
else
|
||||
BODY="Fixes #$ISSUE"
|
||||
fi
|
||||
fi
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh pr create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --body \"$BODY\""
|
||||
[[ -n "$BASE_BRANCH" ]] && CMD="$CMD --base \"$BASE_BRANCH\""
|
||||
[[ -n "$HEAD_BRANCH" ]] && CMD="$CMD --head \"$HEAD_BRANCH\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --label \"$LABELS\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
[[ "$DRAFT" == true ]] && CMD="$CMD --draft"
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
# tea pull create syntax
|
||||
CMD="tea pr create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
||||
[[ -n "$BASE_BRANCH" ]] && CMD="$CMD --base \"$BASE_BRANCH\""
|
||||
[[ -n "$HEAD_BRANCH" ]] && CMD="$CMD --head \"$HEAD_BRANCH\""
|
||||
|
||||
# Handle labels for tea
|
||||
if [[ -n "$LABELS" ]]; then
|
||||
# tea may use --labels flag
|
||||
CMD="$CMD --labels \"$LABELS\""
|
||||
fi
|
||||
|
||||
# Handle milestone for tea
|
||||
if [[ -n "$MILESTONE" ]]; then
|
||||
MILESTONE_ID=$(tea milestones list 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1)
|
||||
if [[ -n "$MILESTONE_ID" ]]; then
|
||||
CMD="$CMD --milestone $MILESTONE_ID"
|
||||
else
|
||||
echo "Warning: Could not find milestone '$MILESTONE', creating without milestone" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Note: tea may not support --draft flag in all versions
|
||||
if [[ "$DRAFT" == true ]]; then
|
||||
echo "Note: Draft PR may not be supported by your tea version" >&2
|
||||
fi
|
||||
|
||||
eval "$CMD"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
88
tools/git/pr-diff.sh
Executable file
88
tools/git/pr-diff.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# pr-diff.sh - Get the diff for a pull request on GitHub or Gitea
|
||||
# Usage: pr-diff.sh -n <pr_number> [-o <output_file>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
OUTPUT_FILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-diff.sh -n <pr_number> [-o <output_file>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -o, --output Output file (optional, prints to stdout if omitted)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform > /dev/null
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
gh pr diff "$PR_NUMBER" > "$OUTPUT_FILE"
|
||||
else
|
||||
gh pr diff "$PR_NUMBER"
|
||||
fi
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
# tea doesn't have a direct diff command — use the API
|
||||
OWNER=$(get_repo_owner)
|
||||
REPO=$(get_repo_name)
|
||||
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
# Extract host from remote URL
|
||||
if [[ "$REMOTE_URL" == https://* ]]; then
|
||||
HOST=$(echo "$REMOTE_URL" | sed -E 's|https://([^/]+)/.*|\1|')
|
||||
elif [[ "$REMOTE_URL" == git@* ]]; then
|
||||
HOST=$(echo "$REMOTE_URL" | sed -E 's|git@([^:]+):.*|\1|')
|
||||
else
|
||||
echo "Error: Cannot determine host from remote URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DIFF_URL="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}.diff"
|
||||
|
||||
# Use tea's auth token if available
|
||||
TEA_TOKEN=$(tea login list 2>/dev/null | grep "$HOST" | awk '{print $NF}' || true)
|
||||
|
||||
if [[ -n "$TEA_TOKEN" ]]; then
|
||||
DIFF_CONTENT=$(curl -sS -H "Authorization: token $TEA_TOKEN" "$DIFF_URL")
|
||||
else
|
||||
DIFF_CONTENT=$(curl -sS "$DIFF_URL")
|
||||
fi
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "$DIFF_CONTENT" > "$OUTPUT_FILE"
|
||||
else
|
||||
echo "$DIFF_CONTENT"
|
||||
fi
|
||||
else
|
||||
echo "Error: Unknown platform" >&2
|
||||
exit 1
|
||||
fi
|
||||
76
tools/git/pr-list.ps1
Normal file
76
tools/git/pr-list.ps1
Normal file
@@ -0,0 +1,76 @@
|
||||
# pr-list.ps1 - List pull requests on Gitea or GitHub
|
||||
# Usage: .\pr-list.ps1 [-State state] [-Label label] [-Author author]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("s")]
|
||||
[ValidateSet("open", "closed", "merged", "all")]
|
||||
[string]$State = "open",
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Label,
|
||||
|
||||
[Alias("a")]
|
||||
[string]$Author,
|
||||
|
||||
[Alias("n")]
|
||||
[int]$Limit = 100,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: pr-list.ps1 [OPTIONS]
|
||||
|
||||
List pull requests from the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-State, -s STATE Filter by state: open, closed, merged, all (default: open)
|
||||
-Label, -l LABEL Filter by label
|
||||
-Author, -a USER Filter by author
|
||||
-Limit, -n N Maximum PRs to show (default: 100)
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\pr-list.ps1 # List open PRs
|
||||
.\pr-list.ps1 -s all # All PRs
|
||||
.\pr-list.ps1 -s merged -a username # Merged PRs by user
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "pr", "list", "--state", $State, "--limit", $Limit)
|
||||
if ($Label) { $cmd += @("--label", $Label) }
|
||||
if ($Author) { $cmd += @("--author", $Author) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "pr", "list", "--state", $State, "--limit", $Limit)
|
||||
|
||||
if ($Label) {
|
||||
Write-Warning "Label filtering may require manual review for Gitea"
|
||||
}
|
||||
if ($Author) {
|
||||
Write-Warning "Author filtering may require manual review for Gitea"
|
||||
}
|
||||
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
93
tools/git/pr-list.sh
Executable file
93
tools/git/pr-list.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
# pr-list.sh - List pull requests on Gitea or GitHub
|
||||
# Usage: pr-list.sh [-s state] [-l label] [-a author]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
STATE="open"
|
||||
LABEL=""
|
||||
AUTHOR=""
|
||||
LIMIT=100
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
List pull requests from the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-s, --state STATE Filter by state: open, closed, merged, all (default: open)
|
||||
-l, --label LABEL Filter by label
|
||||
-a, --author USER Filter by author
|
||||
-n, --limit N Maximum PRs to show (default: 100)
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # List open PRs
|
||||
$(basename "$0") -s all # All PRs
|
||||
$(basename "$0") -s merged -a username # Merged PRs by user
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--state)
|
||||
STATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--label)
|
||||
LABEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--author)
|
||||
AUTHOR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-n|--limit)
|
||||
LIMIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh pr list --state $STATE --limit $LIMIT"
|
||||
[[ -n "$LABEL" ]] && CMD="$CMD --label \"$LABEL\""
|
||||
[[ -n "$AUTHOR" ]] && CMD="$CMD --author \"$AUTHOR\""
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
# tea pr list - note: tea uses 'pulls' subcommand in some versions
|
||||
CMD="tea pr list --state $STATE --limit $LIMIT"
|
||||
|
||||
# tea filtering may be limited
|
||||
if [[ -n "$LABEL" ]]; then
|
||||
echo "Note: Label filtering may require manual review for Gitea" >&2
|
||||
fi
|
||||
if [[ -n "$AUTHOR" ]]; then
|
||||
echo "Note: Author filtering may require manual review for Gitea" >&2
|
||||
fi
|
||||
|
||||
eval "$CMD"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
98
tools/git/pr-merge.ps1
Executable file
98
tools/git/pr-merge.ps1
Executable file
@@ -0,0 +1,98 @@
|
||||
# pr-merge.ps1 - Merge pull requests on Gitea or GitHub
|
||||
# Usage: .\pr-merge.ps1 -Number PR_NUMBER [-Method squash] [-DeleteBranch]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Alias("n")]
|
||||
[int]$Number,
|
||||
|
||||
[Alias("m")]
|
||||
[string]$Method = "squash",
|
||||
|
||||
[Alias("d")]
|
||||
[switch]$DeleteBranch,
|
||||
|
||||
[switch]$SkipQueueGuard,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: pr-merge.ps1 [OPTIONS]
|
||||
|
||||
Merge a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-Number, -n NUMBER PR number to merge (required)
|
||||
-Method, -m METHOD Merge method: squash only (default: squash)
|
||||
-DeleteBranch, -d Delete the head branch after merge
|
||||
-SkipQueueGuard Skip CI queue guard wait before merge
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\pr-merge.ps1 -n 42 # Merge PR #42
|
||||
.\pr-merge.ps1 -n 42 -m squash # Squash merge
|
||||
.\pr-merge.ps1 -n 42 -d # Squash merge and delete branch
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
if ($Method -ne "squash") {
|
||||
Write-Error "Mosaic policy enforces squash merge only. Received '$Method'."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$baseRef = (& gh pr view $Number --json baseRefName --jq ".baseRefName").Trim()
|
||||
if ($baseRef -ne "main") {
|
||||
Write-Error "Mosaic policy allows merges only for PRs targeting 'main' (found '$baseRef')."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $SkipQueueGuard) {
|
||||
$timeout = if ($env:MOSAIC_CI_QUEUE_TIMEOUT_SEC) { [int]$env:MOSAIC_CI_QUEUE_TIMEOUT_SEC } else { 900 }
|
||||
$interval = if ($env:MOSAIC_CI_QUEUE_POLL_SEC) { [int]$env:MOSAIC_CI_QUEUE_POLL_SEC } else { 15 }
|
||||
& "$ScriptDir\ci-queue-wait.ps1" -Purpose merge -Branch $baseRef -TimeoutSeconds $timeout -IntervalSeconds $interval
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
$cmd = @("gh", "pr", "merge", $Number, "--squash")
|
||||
if ($DeleteBranch) { $cmd += "--delete-branch" }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
if (-not $SkipQueueGuard) {
|
||||
$timeout = if ($env:MOSAIC_CI_QUEUE_TIMEOUT_SEC) { [int]$env:MOSAIC_CI_QUEUE_TIMEOUT_SEC } else { 900 }
|
||||
$interval = if ($env:MOSAIC_CI_QUEUE_POLL_SEC) { [int]$env:MOSAIC_CI_QUEUE_POLL_SEC } else { 15 }
|
||||
& "$ScriptDir\ci-queue-wait.ps1" -Purpose merge -Branch "main" -TimeoutSeconds $timeout -IntervalSeconds $interval
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
$cmd = @("tea", "pr", "merge", $Number, "--style", "squash")
|
||||
|
||||
if ($DeleteBranch) {
|
||||
Write-Warning "Branch deletion after merge may need to be done separately with tea"
|
||||
}
|
||||
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "PR #$Number merged successfully"
|
||||
116
tools/git/pr-merge.sh
Executable file
116
tools/git/pr-merge.sh
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/bin/bash
|
||||
# pr-merge.sh - Merge pull requests on Gitea or GitHub
|
||||
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--skip-queue-guard]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
PR_NUMBER=""
|
||||
MERGE_METHOD="squash"
|
||||
DELETE_BRANCH=false
|
||||
SKIP_QUEUE_GUARD=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Merge a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-n, --number NUMBER PR number to merge (required)
|
||||
-m, --method METHOD Merge method: squash only (default: squash)
|
||||
-d, --delete-branch Delete the head branch after merge
|
||||
--skip-queue-guard Skip CI queue guard wait before merge
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -n 42 # Merge PR #42
|
||||
$(basename "$0") -n 42 -m squash # Squash merge
|
||||
$(basename "$0") -n 42 -d # Squash merge and delete branch
|
||||
$(basename "$0") -n 42 --skip-queue-guard # Skip queue guard wait
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--method)
|
||||
MERGE_METHOD="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--delete-branch)
|
||||
DELETE_BRANCH=true
|
||||
shift
|
||||
;;
|
||||
--skip-queue-guard)
|
||||
SKIP_QUEUE_GUARD=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ "$MERGE_METHOD" != "squash" ]]; then
|
||||
echo "Error: Mosaic policy enforces squash merge only. Received '$MERGE_METHOD'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE_BRANCH="$("$SCRIPT_DIR/pr-metadata.sh" -n "$PR_NUMBER" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("baseRefName") or "").strip())')"
|
||||
if [[ "$BASE_BRANCH" != "main" ]]; then
|
||||
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' (found '$BASE_BRANCH')." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_QUEUE_GUARD" != true ]]; then
|
||||
"$SCRIPT_DIR/ci-queue-wait.sh" \
|
||||
--purpose merge \
|
||||
-B "$BASE_BRANCH" \
|
||||
-t "${MOSAIC_CI_QUEUE_TIMEOUT_SEC:-900}" \
|
||||
-i "${MOSAIC_CI_QUEUE_POLL_SEC:-15}"
|
||||
fi
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh pr merge $PR_NUMBER --squash"
|
||||
[[ "$DELETE_BRANCH" == true ]] && CMD="$CMD --delete-branch"
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
# tea pr merge syntax
|
||||
CMD="tea pr merge $PR_NUMBER --style squash"
|
||||
|
||||
# Delete branch after merge if requested
|
||||
if [[ "$DELETE_BRANCH" == true ]]; then
|
||||
echo "Note: Branch deletion after merge may need to be done separately with tea" >&2
|
||||
fi
|
||||
|
||||
eval "$CMD"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "PR #$PR_NUMBER merged successfully"
|
||||
114
tools/git/pr-metadata.sh
Executable file
114
tools/git/pr-metadata.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
# pr-metadata.sh - Get PR metadata as JSON on GitHub or Gitea
|
||||
# Usage: pr-metadata.sh -n <pr_number> [-o <output_file>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
OUTPUT_FILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-metadata.sh -n <pr_number> [-o <output_file>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -o, --output Output file (optional, prints to stdout if omitted)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform > /dev/null
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
METADATA=$(gh pr view "$PR_NUMBER" --json number,title,body,state,author,headRefName,baseRefName,files,labels,assignees,milestone,createdAt,updatedAt,url,isDraft)
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "$METADATA" > "$OUTPUT_FILE"
|
||||
else
|
||||
echo "$METADATA"
|
||||
fi
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
OWNER=$(get_repo_owner)
|
||||
REPO=$(get_repo_name)
|
||||
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
# Extract host from remote URL
|
||||
if [[ "$REMOTE_URL" == https://* ]]; then
|
||||
HOST=$(echo "$REMOTE_URL" | sed -E 's|https://([^/]+)/.*|\1|')
|
||||
elif [[ "$REMOTE_URL" == git@* ]]; then
|
||||
HOST=$(echo "$REMOTE_URL" | sed -E 's|git@([^:]+):.*|\1|')
|
||||
else
|
||||
echo "Error: Cannot determine host from remote URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_URL="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}"
|
||||
|
||||
# Use tea's auth token if available
|
||||
TEA_TOKEN=$(tea login list 2>/dev/null | grep "$HOST" | awk '{print $NF}' || true)
|
||||
|
||||
if [[ -n "$TEA_TOKEN" ]]; then
|
||||
RAW=$(curl -sS -H "Authorization: token $TEA_TOKEN" "$API_URL")
|
||||
else
|
||||
RAW=$(curl -sS "$API_URL")
|
||||
fi
|
||||
|
||||
# Normalize Gitea response to match our expected schema
|
||||
METADATA=$(echo "$RAW" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
normalized = {
|
||||
'number': data.get('number'),
|
||||
'title': data.get('title'),
|
||||
'body': data.get('body', ''),
|
||||
'state': data.get('state'),
|
||||
'author': data.get('user', {}).get('login', ''),
|
||||
'headRefName': data.get('head', {}).get('ref', ''),
|
||||
'baseRefName': data.get('base', {}).get('ref', ''),
|
||||
'labels': [l.get('name', '') for l in data.get('labels', [])],
|
||||
'assignees': [a.get('login', '') for a in data.get('assignees', [])],
|
||||
'milestone': data.get('milestone', {}).get('title', '') if data.get('milestone') else '',
|
||||
'createdAt': data.get('created_at', ''),
|
||||
'updatedAt': data.get('updated_at', ''),
|
||||
'url': data.get('html_url', ''),
|
||||
'isDraft': data.get('draft', False),
|
||||
'mergeable': data.get('mergeable'),
|
||||
'diffUrl': data.get('diff_url', ''),
|
||||
}
|
||||
json.dump(normalized, sys.stdout, indent=2)
|
||||
")
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "$METADATA" > "$OUTPUT_FILE"
|
||||
else
|
||||
echo "$METADATA"
|
||||
fi
|
||||
else
|
||||
echo "Error: Unknown platform" >&2
|
||||
exit 1
|
||||
fi
|
||||
115
tools/git/pr-review.sh
Executable file
115
tools/git/pr-review.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
# pr-review.sh - Review a pull request on GitHub or Gitea
|
||||
# Usage: pr-review.sh -n <pr_number> -a <action> [-c <comment>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
ACTION=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--action)
|
||||
ACTION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-review.sh -n <pr_number> -a <action> [-c <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -a, --action Review action: approve, request-changes, comment (required)"
|
||||
echo " -c, --comment Review comment (required for request-changes)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$ACTION" ]]; then
|
||||
echo "Error: Action is required (-a): approve, request-changes, comment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
case $ACTION in
|
||||
approve)
|
||||
gh pr review "$PR_NUMBER" --approve ${COMMENT:+--body "$COMMENT"}
|
||||
echo "Approved GitHub PR #$PR_NUMBER"
|
||||
;;
|
||||
request-changes)
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment required for request-changes"
|
||||
exit 1
|
||||
fi
|
||||
gh pr review "$PR_NUMBER" --request-changes --body "$COMMENT"
|
||||
echo "Requested changes on GitHub PR #$PR_NUMBER"
|
||||
;;
|
||||
comment)
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment required"
|
||||
exit 1
|
||||
fi
|
||||
gh pr review "$PR_NUMBER" --comment --body "$COMMENT"
|
||||
echo "Added review comment to GitHub PR #$PR_NUMBER"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown action: $ACTION"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
case $ACTION in
|
||||
approve)
|
||||
tea pr approve "$PR_NUMBER" ${COMMENT:+--comment "$COMMENT"}
|
||||
echo "Approved Gitea PR #$PR_NUMBER"
|
||||
;;
|
||||
request-changes)
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment required for request-changes"
|
||||
exit 1
|
||||
fi
|
||||
tea pr reject "$PR_NUMBER" --comment "$COMMENT"
|
||||
echo "Requested changes on Gitea PR #$PR_NUMBER"
|
||||
;;
|
||||
comment)
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment required"
|
||||
exit 1
|
||||
fi
|
||||
tea pr comment "$PR_NUMBER" "$COMMENT"
|
||||
echo "Added comment to Gitea PR #$PR_NUMBER"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown action: $ACTION"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
48
tools/git/pr-view.sh
Executable file
48
tools/git/pr-view.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# pr-view.sh - View pull request details on GitHub or Gitea
|
||||
# Usage: pr-view.sh -n <pr_number>
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-view.sh -n <pr_number>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh pr view "$PR_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea pr "$PR_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user