jq for Git Forge CLIs
Manage repositories at scale across multiple forges. These patterns work with gh (GitHub), glab (GitLab), and tea (Gitea).
GitHub CLI (gh)
Repository Listing
# List all repos with key fields
gh repo list --limit 100 --json name,visibility,pushedAt | jq '.[] | {
name,
visibility,
last_push: .pushedAt | split("T")[0]
}'
# Private repos only
gh repo list --limit 100 --json name,visibility | jq '
.[] | select(.visibility == "PRIVATE") | .name
'
# Repos with specific topic
gh repo list --json name,repositoryTopics | jq '
.[] | select(.repositoryTopics | map(.name) | contains(["infrastructure"])) | .name
'
# Repos pushed in last 7 days
gh repo list --limit 100 --json name,pushedAt | jq '
(now - 604800) as $week_ago |
.[] | select(.pushedAt | fromdateiso8601 > $week_ago) |
{name, pushedAt}
'
Repository Details
# Get repo name for scripts
REPO_NAME=$(gh repo view --json name -q '.name')
# Full repo info
gh repo view --json name,owner,sshUrl,description | jq '.'
# Check if repo has issues enabled
gh repo view --json hasIssuesEnabled -q '.hasIssuesEnabled'
# Get default branch
gh repo view --json defaultBranchRef -q '.defaultBranchRef.name'
Issue and PR Management
# List open issues with labels
gh issue list --json number,title,labels,createdAt | jq '.[] | {
number,
title,
labels: [.labels[].name],
age: (((now - (.createdAt | fromdateiso8601)) / 86400) | floor | "\(.)d")
}'
# Issues without assignee
gh issue list --json number,title,assignees | jq '
.[] | select(.assignees | length == 0) | {number, title}
'
# PR review status
gh pr list --json number,title,reviewDecision,author | jq '.[] | {
number,
title,
author: .author.login,
status: .reviewDecision
}'
# Stale PRs (open > 14 days)
gh pr list --json number,title,createdAt | jq '
(now - 1209600) as $two_weeks_ago |
.[] | select(.createdAt | fromdateiso8601 < $two_weeks_ago) |
{
number,
title,
age: (((now - (.createdAt | fromdateiso8601)) / 86400) | floor | "\(.)d")
}
'
Workflow Runs
# Recent workflow runs
gh run list --json databaseId,displayTitle,status,conclusion,createdAt | jq '.[] | {
id: .databaseId,
title: .displayTitle,
status: .status,
conclusion: .conclusion,
started: .createdAt | split("T")[0]
}'
# Failed runs only
gh run list --json databaseId,displayTitle,conclusion | jq '
.[] | select(.conclusion == "failure") | {id: .databaseId, title: .displayTitle}
'
# Run duration analysis
gh run list --limit 20 --json databaseId,displayTitle,createdAt,updatedAt | jq '.[] | {
title: .displayTitle,
duration_mins: (
((.updatedAt | fromdateiso8601) - (.createdAt | fromdateiso8601)) / 60 | floor
)
}'
Release Management
# List releases with assets
gh release list --json tagName,publishedAt,isPrerelease | jq '.[] | {
tag: .tagName,
published: .publishedAt | split("T")[0],
prerelease: .isPrerelease
}'
# Latest release info
gh release view --json tagName,body,assets | jq '{
tag: .tagName,
assets: [.assets[] | {name, download_count: .downloadCount}],
notes: .body | split("\n")[0:5] | join("\n")
}'
API Direct Access
# Any GitHub API endpoint
gh api repos/{owner}/{repo} | jq '{
name,
stars: .stargazers_count,
forks: .forks_count,
open_issues: .open_issues_count
}'
# GraphQL query
gh api graphql -f query='
query {
viewer {
repositories(first: 10, orderBy: {field: PUSHED_AT, direction: DESC}) {
nodes { name pushedAt }
}
}
}
' | jq '.data.viewer.repositories.nodes'
GitLab CLI (glab)
Repository Listing
# List repos (projects)
glab repo list --output json | jq '.[] | {
name: .path,
visibility,
last_activity: .last_activity_at | split("T")[0]
}'
# Private repos only
glab repo list --output json | jq '
.[] | select(.visibility == "private") | .path
'
# Repos with recent activity (last 30 days)
glab repo list --output json | jq '
(now - 2592000) as $month_ago |
.[] | select(.last_activity_at | fromdateiso8601 > $month_ago) |
{name: .path, last_activity: .last_activity_at}
'
SSH URLs for Multi-Forge Setup
# Extract SSH URLs for cloning
glab repo list --output json | jq -r '.[] | .ssh_url_to_repo'
# Create remote add commands
glab repo list --output json | jq -r '
.[] | "git remote add gitlab \(.ssh_url_to_repo)"
'
Merge Request Management
# List open MRs
glab mr list --output json | jq '.[] | {
iid,
title,
author: .author.username,
draft: .draft,
created: .created_at | split("T")[0]
}'
# MRs awaiting review
glab mr list --output json | jq '
.[] | select(.reviewers | length > 0) |
select(.approved == false) |
{iid, title, reviewers: [.reviewers[].username]}
'
# MR with pipeline status
glab mr list --output json | jq '.[] | {
iid,
title,
pipeline: .head_pipeline.status
}'
Pipeline Analysis
# Recent pipelines
glab api projects/:id/pipelines | jq '.[] | {
id,
status,
ref,
duration: (if .duration then "\(.duration)s" else "running" end),
created: .created_at | split("T")[0]
}'
# Failed pipelines with job details
glab api projects/:id/pipelines | jq '
.[] | select(.status == "failed") | .id
' | head -5 | while read pipeline_id; do
glab api "projects/:id/pipelines/$pipeline_id/jobs" | jq --arg pid "$pipeline_id" '
.[] | select(.status == "failed") | {
pipeline: $pid,
job: .name,
stage: .stage,
failure_reason: .failure_reason
}
'
done
Gitea CLI (tea)
Repository Listing
# List repos
tea repo list --output json | jq '.[] | {
name,
private,
clone_url,
updated: .updated_at | split("T")[0]
}'
# Private repos only
tea repo list --output json | jq '
.[] | select(.private == true) | .name
'
Cross-Forge Patterns
Sync Repo Names Across Forges
# Extract unique repo names across all forges
{
gh repo list --json name -q '.[].name'
glab repo list --output json | jq -r '.[].path'
tea repo list --output json | jq -r '.[].name'
} | sort -u > all_repos.txt
Multi-Forge Status Check
#!/bin/bash
# Check if repo exists on all forges
REPO_NAME="${1:?Usage: check-repo.sh <repo-name>}"
echo "=== GitHub ==="
gh repo view "EvanusModestus/$REPO_NAME" --json name 2>/dev/null && echo "EXISTS" || echo "NOT FOUND"
echo "=== GitLab ==="
glab repo view "EvanusModestus/$REPO_NAME" --output json 2>/dev/null | jq -r '.path' && echo "EXISTS" || echo "NOT FOUND"
echo "=== Gitea ==="
tea repo list --output json | jq -r --arg r "$REPO_NAME" '.[] | select(.name == $r) | .name' | grep -q . && echo "EXISTS" || echo "NOT FOUND"
Create Repo on All Forges
#!/bin/bash
# create-everywhere.sh - Create repo on all forges
REPO_NAME="${1:?Usage: create-everywhere.sh <repo-name>}"
echo "Creating $REPO_NAME on all forges..."
# GitHub
gh repo create "$REPO_NAME" --private --source=. --push 2>/dev/null && echo "✓ GitHub" || echo "✗ GitHub (may exist)"
# GitLab
glab repo create "$REPO_NAME" --private 2>/dev/null && echo "✓ GitLab" || echo "✗ GitLab (may exist)"
# Gitea
tea repo create --name "$REPO_NAME" --private 2>/dev/null && echo "✓ Gitea" || echo "✗ Gitea (may exist)"
# Add remotes
git remote add gitlab "git@gitlab.com:EvanusModestus/$REPO_NAME.git" 2>/dev/null
git remote add gitea "git@gitea:evanusmodestus/$REPO_NAME.git" 2>/dev/null
echo "Remotes configured:"
git remote -v
Export Repo List to JSON
# Unified repo inventory across forges
{
echo '{"github":'
gh repo list --limit 200 --json name,visibility,pushedAt
echo ',"gitlab":'
glab repo list --output json
echo ',"gitea":'
tea repo list --output json
echo '}'
} | jq '
{
github: [.github[] | {name, visibility, lastPush: .pushedAt}],
gitlab: [.gitlab[] | {name: .path, visibility, lastPush: .last_activity_at}],
gitea: [.gitea[] | {name, visibility: (if .private then "private" else "public" end), lastPush: .updated_at}]
}
' > repo_inventory.json
Useful Variables
# Get repo name from current directory
REPO_NAME=$(gh repo view --json name -q '.name')
# Get owner/org
OWNER=$(gh repo view --json owner -q '.owner.login')
# Get default branch
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef -q '.defaultBranchRef.name')
# Use in subsequent commands
gh api "repos/$OWNER/$REPO_NAME/branches/$DEFAULT_BRANCH/protection"
Related
-
Security Scanning - Dependabot/vulnerability patterns
-
Git Repository Operations (infra-ops) - Multi-forge workflow runbook