
Gerrit
- 109 installs
- 12 repo stars
- Updated August 4, 2026
- odyssey4me/agent-skills
Helps with ai & agent building tasks.
About
gerrit is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- gerrit
- AI & Agent Building
- AI-coding skill
Gerrit by the numbers
- 109 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #4,071 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/odyssey4me/agent-skills --skill gerritAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 109 |
|---|---|
| repo stars | ★ 12 |
| Last updated | August 4, 2026 |
| Repository | odyssey4me/agent-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Gerrit Skill
This skill provides Gerrit code review integration using git-review with a Python wrapper for markdown-formatted query output on read/view operations. Action commands (submit, review, abandon) should use git-review or SSH commands directly.
Prerequisites
Install git-review: pip install git-review — docs
Authentication
git-review uses SSH for authentication with Gerrit servers.
# Configure Gerrit username (if different from local user)
git config --global gitreview.username yourgerrituser
# Test SSH connection
ssh -p 29418 youruser@review.example.com gerrit version
# Add SSH key to Gerrit
# 1. Generate SSH key if needed: ssh-keygen -t ed25519
# 2. Copy public key: cat ~/.ssh/id_ed25519.pub
# 3. Add to Gerrit: Settings > SSH KeysGerrit supports multiple authentication methods:
- SSH (recommended): Used by git-review for all operations
- HTTP/HTTPS: For web UI and REST API access (set password in Settings > HTTP Password)
See Gerrit Authentication for details.
Initial Setup
Configure Repository
# One-time setup for a repository
git review -s
# Or manually create .gitreview file in repository root
cat > .gitreview <<EOF
[gerrit]
host=review.example.com
port=29418
project=myproject
defaultbranch=main
EOFSee Installation Guide for details.
Script Usage
The wrapper script (scripts/gerrit.py) uses Gerrit SSH query commands and formats output as markdown. Connection details are read from .gitreview or provided via --host/--port/--username flags. See permissions.md for read/write classification of each command.
# Check Gerrit SSH access
$SKILL_DIR/scripts/gerrit.py check
# Changes
$SKILL_DIR/scripts/gerrit.py changes list
$SKILL_DIR/scripts/gerrit.py changes view 12345
$SKILL_DIR/scripts/gerrit.py changes search "status:open project:myproject"
# Projects
$SKILL_DIR/scripts/gerrit.py projects listAll commands support --limit N for list commands (default 30).
Global connection options: --host, --port (default 29418), --username.
Commands (Direct git-review Usage)
For action commands, use git-review or SSH commands directly:
Submitting Changes
git review # Submit current branch for review
git review -t topic-name # Submit with topic
git review -f # Submit and close local branch
git review --reviewers user1,user2 # Add reviewers
git review -n # Dry-run (show what would be done)Full reference: git-review usage
Downloading Changes
git review -d 12345 # Download change 12345
git review -d 12345,3 # Download patchset 3 of change 12345
git review -x 12345 # Cherry-pick change (no branch)
git review -m 12345 # Compare local changes to remoteDownloads create a local branch named review/username/topic.
Updating Changes
# Make changes to downloaded review
git commit --amend
git review # Upload new patchset
# Update to latest patchset
git review -d 12345 # Re-download updates the branchAdvanced Options
git review -R # Don't rebase (submit as-is)
git review -D # Draft mode (WIP changes)
git review --no-cache # Skip local cache
git review -v # Verbose output
git review --track # Track remote branchConfiguration
Per-Repository Settings
File: .gitreview (repository root)
[gerrit]
host=review.example.com
port=29418
project=myproject/subproject
defaultbranch=main
defaultremote=originGlobal Settings
# Set Gerrit username
git config --global gitreview.username myuser
# Set default remote
git config --global gitreview.remote gerrit
# Configure scheme (ssh/http/https)
git config --global gitreview.scheme sshConfiguration stored in ~/.gitconfig
Examples
Daily Workflow
# Start work on new feature
git checkout -b feature-branch
# ... make changes ...
git commit -m "Add new feature"
# Submit for review
git review -t feature-topic
# Verify submission
$SKILL_DIR/scripts/gerrit.py changes list # confirm change appears
# Address review comments
# ... make changes ...
git commit --amend
git review
# Verify new patchset uploaded
$SKILL_DIR/scripts/gerrit.py changes view <change-number>Reviewing Others' Changes
# Download change for review
git review -d 12345
# Verify download
$SKILL_DIR/scripts/gerrit.py changes view 12345
# Test the change
# ... run tests, verify code ...
# Return to main branch
git checkout main
git branch -D review/user/topicWorking with Topics
# Submit with topic
git review -t authentication-refactor
# Verify submission
$SKILL_DIR/scripts/gerrit.py changes list
# All related changes will be grouped under this topic
git commit -m "Part 2: Update tests"
git review -t authentication-refactorSee common-workflows.md for more examples.
Advanced Usage
See advanced-usage.md for SSH commands, JSON output, and multi-server configuration.
Model Guidance
This skill wraps an official CLI. A fast, lightweight model is sufficient.
Troubleshooting
See troubleshooting.md for common issues and fixes.
Official Documentation
- git-review Manual: <https://docs.opendev.org/opendev/git-review/latest/>
- git-review Repository: <https://opendev.org/opendev/git-review>
- Gerrit Documentation: <https://gerrit-review.googlesource.com/Documentation/>
- Gerrit SSH Commands: <https://gerrit-review.googlesource.com/Documentation/cmd-index.html>
Advanced Usage
SSH Commands
For operations not covered by git-review:
# Query open changes
ssh -p 29418 review.example.com gerrit query status:open project:myproject
# Query specific change
ssh -p 29418 review.example.com gerrit query change:12345
# Review from command line
ssh -p 29418 review.example.com gerrit review 12345,3 --verified +1 --message "'Looks good'"
# Abandon change
ssh -p 29418 review.example.com gerrit review 12345 --abandonFull reference: Gerrit SSH Commands
JSON Output for Scripting
# Get change info as JSON
ssh -p 29418 review.example.com gerrit query --format=JSON change:12345
# Process with jq
ssh -p 29418 review.example.com gerrit query --format=JSON status:open | jq '.subject'Multiple Gerrit Servers
# Set remote for specific server
git config gitreview.remote gerrit-prod
# Or specify via command line
git review -r gerrit-stagingCommon Gerrit Workflows with git-review
This document provides practical examples of common Gerrit code review workflows using git-review.
Table of Contents
- Getting Started
- Submitting Changes
- Reviewing Changes
- Working with Patchsets
- Topics and Dependencies
- Advanced Workflows
- SSH Commands
- Automation Examples
Getting Started
Initial Repository Setup
# Clone repository
git clone ssh://review.example.com:29418/myproject
cd myproject
# Setup git-review (one-time)
git review -s
# Verify setup
cat .gitreviewConfigure Commit Hook
# Install commit-msg hook for Change-Id generation
git review -s
# Verify hook is installed
ls -l .git/hooks/commit-msg
# Test with a commit
git commit --allow-empty -m "test: verify Change-Id generation"
git log -1
# Should see "Change-Id: I..." in commit messageSubmitting Changes
Basic Submission Workflow
# Create feature branch
git checkout -b feature/new-api
git checkout main
# Make changes
vim api/endpoints.py
# Commit with descriptive message
git commit -m "Add new API endpoint for user profiles
This adds a REST endpoint that returns user profile data.
Change-Id: Ixxx # Auto-generated by commit hook
"
# Submit for review
git reviewSubmit with Topic
# Group related changes under a topic
git commit -m "Part 1: Add user model"
git review -t user-profile-feature
# Additional changes in same topic
git checkout main
git checkout -b feature/user-api
# ... make changes ...
git commit -m "Part 2: Add user API"
git review -t user-profile-featureSubmit with Reviewers
# Add reviewers when submitting
git review --reviewers alice@example.com,bob@example.com
# Add reviewers for specific areas
git review \
--reviewers backend-team@example.com \
--reviewers security-team@example.comSubmit Draft/WIP Changes
# Submit as work-in-progress
git review -D
# Later, publish the change via Gerrit web UI
# or use SSH:
ssh -p 29418 review.example.com gerrit review 12345 --publishDry-Run Before Submitting
# See what would be submitted
git review -n
# Review the output, then submit
git reviewReviewing Changes
Download Change for Review
# Download specific change
git review -d 12345
# This creates branch: review/username/topic
git branch
# * review/alice/user-api
# main
# Review the code
git log -p HEAD^..HEAD
git diff main..HEAD
# Test the change
make test
python -m pytest
# Return to main
git checkout mainDownload Specific Patchset
# Download patchset 3 of change 12345
git review -d 12345,3
# Compare patchsets
git review -d 12345,2
PATCHSET_2=$(git rev-parse HEAD)
git review -d 12345,3
git diff $PATCHSET_2..HEADCherry-pick for Quick Testing
# Apply change without creating branch
git checkout main
git review -x 12345
# Now the change is on main (don't push!)
# Test it
make test
# Discard when done
git reset --hard origin/mainCompare Changes
# Compare your local work to remote change
git review -m 12345
# Shows diff between your changes and the reviewWorking with Patchsets
Update Your Change
# Download your change
git review -d 12345
# Make requested changes
vim api/endpoints.py
# Amend the commit (preserves Change-Id)
git commit --amend
# Upload new patchset
git reviewRebase on Latest Main
# Download your change
git review -d 12345
# Rebase on latest main
git fetch origin
git rebase origin/main
# Upload rebased patchset
git reviewSubmit Without Rebase
# Submit exactly as-is (no rebase)
git review -R
# Useful when you've carefully tested specific commitsSplit a Change
# Download the change
git review -d 12345
# Reset to split commits
git reset HEAD^
# Create first commit
git add file1.py
git commit -m "Part 1: Add data model"
git review -t feature-name
# Create second commit
git add file2.py
git commit -m "Part 2: Add API endpoint"
git review -t feature-nameSquash Multiple Commits
# If you have multiple commits that should be one
git rebase -i HEAD~3
# In editor, change:
# pick abc123 commit 1
# pick def456 commit 2
# pick ghi789 commit 3
# To:
# pick abc123 commit 1
# squash def456 commit 2
# squash ghi789 commit 3
# Upload squashed change
git reviewTopics and Dependencies
Create Topic Chain
# First change in series
git checkout main
git checkout -b feature/step1
# ... make changes ...
git commit -m "Step 1: Add database schema"
git review -t multi-step-feature
# Second change (depends on first)
git checkout -b feature/step2
# ... make changes ...
git commit -m "Step 2: Add API endpoint"
git review -t multi-step-feature
# Third change
git checkout -b feature/step3
# ... make changes ...
git commit -m "Step 3: Add UI components"
git review -t multi-step-featureUpdate Change in Chain
# Download the middle change
git review -d 12346
# Make updates
git commit --amend
# Rebase dependent changes
git review
# Update dependent changes too
git review -d 12347
git rebase review/user/step2
git reviewView All Changes in Topic
# Query changes by topic
ssh -p 29418 review.example.com \
gerrit query topic:multi-step-feature status:open
# Or via web:
# https://review.example.com/#/q/topic:multi-step-featureAdvanced Workflows
Work on Multiple Changes
# Create multiple independent changes
git checkout main
git checkout -b feature/change1
# ... work ...
git commit -m "Add feature 1"
git review
git checkout main
git checkout -b feature/change2
# ... work ...
git commit -m "Add feature 2"
git review
# List local review branches
git branch | grep review/Recover from Mistakes
# Forgot to amend, created new change instead
# Download the wrong new change
git review -d 12347
# Check both commits
git log -2 --oneline
# Squash them
git rebase -i HEAD~2
# Mark second commit as 'squash'
# Upload corrected change
git reviewBackport to Release Branch
# Download change from main
git review -d 12345
# Create backport branch
git checkout -b backport/stable-2.1 origin/stable/2.1
# Cherry-pick the change
git cherry-pick review/user/feature
# Resolve conflicts if needed
# ... fix conflicts ...
git add .
git cherry-pick --continue
# Submit to stable branch
git reviewFinish and Clean Up
# Submit and clean up local branch
git review -f
# This does:
# 1. Submits the change
# 2. Deletes local branch
# 3. Switches back to mainSSH Commands
Query Changes
# List open changes
ssh -p 29418 review.example.com gerrit query status:open
# List your changes
ssh -p 29418 review.example.com gerrit query owner:self status:open
# Find changes by topic
ssh -p 29418 review.example.com gerrit query topic:feature-name
# Complex query
ssh -p 29418 review.example.com gerrit query \
'project:myproject AND status:open AND -age:7d'
# Get JSON output
ssh -p 29418 review.example.com gerrit query \
--format=JSON status:open project:myprojectReview from Command Line
# Approve change
ssh -p 29418 review.example.com gerrit review \
12345,3 --verified +1 --code-review +2 --message "'LGTM!'"
# Request changes
ssh -p 29418 review.example.com gerrit review \
12345,3 --code-review -1 --message "'Please fix the error handling'"
# Submit change
ssh -p 29418 review.example.com gerrit review \
12345,3 --submitAbandon/Restore Changes
# Abandon change
ssh -p 29418 review.example.com gerrit review \
12345 --abandon --message "'No longer needed'"
# Restore change
ssh -p 29418 review.example.com gerrit review \
12345 --restore --message "'Actually still needed'"Manage Topics
# Set topic
ssh -p 29418 review.example.com gerrit set-topic \
12345 feature-name
# Remove topic
ssh -p 29418 review.example.com gerrit set-topic \
12345 ""Automation Examples
Daily Review Queue
#!/bin/bash
# Check your review queue each morning
echo "=== Changes Waiting for Your Review ==="
ssh -p 29418 review.example.com gerrit query \
--format=JSON \
'status:open AND reviewer:self -owner:self' | \
jq -r 'select(.number != null) | " #\(.number): \(.subject) (@\(.owner.username))"'
echo -e "\n=== Your Changes Needing Attention ==="
ssh -p 29418 review.example.com gerrit query \
--format=JSON \
'status:open AND owner:self' | \
jq -r 'select(.number != null) | " #\(.number): \(.subject) (Score: \(.currentPatchSet.approvals[0].value // "not reviewed"))"'Auto-sync with Latest Changes
#!/bin/bash
# Update all local review branches
for branch in $(git branch | grep review/); do
echo "Updating $branch..."
git checkout $branch
# Fetch latest patchset
CHANGE_NUM=$(git log -1 --format=%B | grep -oP 'Change-Id: I\K[a-f0-9]+' | head -1)
if [ -n "$CHANGE_NUM" ]; then
# Find change number from Change-Id
NUM=$(ssh -p 29418 review.example.com gerrit query \
--format=JSON change:I$CHANGE_NUM | \
jq -r 'select(.number != null) | .number')
if [ -n "$NUM" ]; then
git review -d $NUM
fi
fi
done
git checkout mainSubmit Change Chain
#!/bin/bash
# Submit multiple changes in order
TOPIC="feature-name"
# Get all changes in topic, sorted by number
ssh -p 29418 review.example.com gerrit query \
--format=JSON topic:$TOPIC status:open | \
jq -r 'select(.number != null) | .number' | \
sort -n | \
while read num; do
echo "Reviewing change $num..."
# Download and test
git review -d $num
# Run tests
if make test; then
echo "Tests passed for $num"
# Approve via SSH
ssh -p 29418 review.example.com gerrit review \
$num --verified +1 --message "'Automated verification passed'"
else
echo "Tests failed for $num - stopping"
break
fi
done
git checkout mainMonitor Change Status
#!/bin/bash
# Monitor specific change for updates
CHANGE_NUM=$1
INTERVAL=${2:-60} # Check every 60 seconds
echo "Monitoring change $CHANGE_NUM..."
LAST_UPDATED=""
while true; do
INFO=$(ssh -p 29418 review.example.com gerrit query \
--format=JSON $CHANGE_NUM)
UPDATED=$(echo "$INFO" | jq -r '.lastUpdated')
STATUS=$(echo "$INFO" | jq -r '.status')
if [ "$UPDATED" != "$LAST_UPDATED" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Change $CHANGE_NUM updated - Status: $STATUS"
# Show latest comments
echo "$INFO" | jq -r '.comments[]? | " \(.reviewer.username): \(.message)"'
LAST_UPDATED=$UPDATED
# Exit if merged or abandoned
if [ "$STATUS" = "MERGED" ] || [ "$STATUS" = "ABANDONED" ]; then
echo "Change $STATUS - exiting monitor"
break
fi
fi
sleep $INTERVAL
doneGenerate Weekly Report
#!/bin/bash
# Weekly activity report
WEEK_AGO=$(date -d '7 days ago' '+%Y-%m-%d')
echo "=== Gerrit Activity Report (last 7 days) ==="
echo -e "\nChanges Merged:"
ssh -p 29418 review.example.com gerrit query \
--format=JSON \
"status:merged AND after:$WEEK_AGO" | \
jq -r 'select(.number != null) | " #\(.number): \(.subject) (@\(.owner.username))"'
echo -e "\nChanges Abandoned:"
ssh -p 29418 review.example.com gerrit query \
--format=JSON \
"status:abandoned AND after:$WEEK_AGO" | \
jq -r 'select(.number != null) | " #\(.number): \(.subject) (@\(.owner.username))"'
echo -e "\nActive Changes (still open):"
ssh -p 29418 review.example.com gerrit query \
--format=JSON \
"status:open AND after:$WEEK_AGO" | \
jq -r 'select(.number != null) | " #\(.number): \(.subject) (@\(.owner.username))"'Tips for Efficient Workflows
Git Aliases
Add to ~/.gitconfig:
[alias]
# git-review shortcuts
gr = review
grd = review -d
grx = review -x
grf = review -f
grn = review -nUsage:
git gr # Submit review
git grd 12345 # Download change
git grx 12345 # Cherry-pick change
git grf # Submit and finishBash Functions
Add to ~/.bashrc:
# Download and checkout Gerrit change
gr() {
git review -d "$1"
}
# Query Gerrit changes
gq() {
ssh -p 29418 review.example.com gerrit query "$@"
}
# Review from command line
gapprove() {
ssh -p 29418 review.example.com gerrit review \
"$1" --verified +1 --code-review +2 --message "'$2'"
}
# List my open changes
mychanges() {
ssh -p 29418 review.example.com gerrit query \
--format=JSON 'owner:self status:open' | \
jq -r 'select(.number != null) | "\(.number)\t\(.subject)"'
}Environment Variables
# Set Gerrit username
export GERRIT_USER=myusername
# Create wrapper for SSH
gerrit() {
ssh -p 29418 $GERRIT_USER@review.example.com gerrit "$@"
}
# Usage:
gerrit query status:open
gerrit review 12345 --code-review +2Pre-push Hook
Validate before pushing to Gerrit:
# .git/hooks/pre-push
#!/bin/bash
# Check for Change-Id
if ! git log -1 --format=%B | grep -q '^Change-Id:'; then
echo "Error: Missing Change-Id in commit message"
echo "Run: git review -s (to install commit-msg hook)"
exit 1
fi
# Run tests
if ! make test; then
echo "Error: Tests failed"
exit 1
fi
exit 0chmod +x .git/hooks/pre-pushAdditional Resources
Command Permissions
This reference classifies commands by access level to help agents enforce appropriate permission controls.
- read: Safe to execute without user confirmation. These commands
only retrieve or display information.
- write: Requires user confirmation before execution. These
commands create, modify, or delete data.
Note: This skill's script only provides read operations. Write operations use git-review directly and are not covered here.
| Command | Access | Description |
|---|---|---|
| check | read | Verify setup and connectivity |
| changes list | read | List changes |
| changes view | read | View change details |
| changes search | read | Search changes |
| projects list | read | List projects |
Troubleshooting
# Re-run setup
git review -s
# Force setup (fixes common issues)
git review -s --force
# Verbose output for debugging
git review -v
# Check configuration
cat .gitreview
git config -l | grep gitreview
# Test SSH connection
ssh -p 29418 youruser@review.example.com gerrit versionCommon Issues
"We don't know where your gerrit is"
git review -s # Run setup
# Or create .gitreview file manually"fatal: 'gerrit' does not appear to be a git repository"
git review -s # Setup remote
git remote -v # Verify gerrit remote exists"Permission denied (publickey)"
# Add SSH key to Gerrit (Settings > SSH Keys)
# Or configure username:
git config --global gitreview.username youruserChange-Id missing
# Install commit-msg hook
curl -Lo .git/hooks/commit-msg \
https://review.example.com/tools/hooks/commit-msg
chmod u+x .git/hooks/commit-msg
# Or let git-review install it
git review -s#!/usr/bin/env python3
"""Gerrit wrapper skill for AI agents.
Wraps Gerrit SSH query commands to produce markdown-formatted output for
read/view operations. Action commands (review, abandon, submit) should use
SSH gerrit commands directly.
Usage:
python gerrit.py check
python gerrit.py changes list
python gerrit.py changes view 12345
python gerrit.py changes search "status:open project:myproject"
python gerrit.py projects list
Requirements:
SSH access to a Gerrit server (typically configured via .gitreview)
"""
from __future__ import annotations
import argparse
import configparser
import json
import subprocess
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
# ============================================================================
# SSH / GERRIT HELPERS
# ============================================================================
def _read_gitreview(path: str | None = None) -> dict[str, str]:
"""Parse .gitreview file for Gerrit connection details.
Args:
path: Path to .gitreview file. Defaults to .gitreview in cwd.
Returns:
Dict with host, port, project, username keys (values may be empty).
"""
gitreview_path = Path(path) if path else Path(".gitreview")
result: dict[str, str] = {"host": "", "port": "29418", "project": "", "username": ""}
if not gitreview_path.exists():
return result
config = configparser.ConfigParser()
config.read(str(gitreview_path))
if config.has_section("gerrit"):
result["host"] = config.get("gerrit", "host", fallback="")
result["port"] = config.get("gerrit", "port", fallback="29418")
result["project"] = config.get("gerrit", "project", fallback="")
return result
def _get_ssh_cmd(host: str, port: str = "29418", username: str | None = None) -> list[str]:
"""Build SSH command prefix for Gerrit.
Args:
host: Gerrit server hostname.
port: SSH port (default 29418).
username: SSH username (optional).
Returns:
List of command parts for SSH connection.
"""
cmd = ["ssh", "-p", port]
if username:
cmd.append(f"{username}@{host}")
else:
cmd.append(host)
return cmd
def run_gerrit_query(
host: str,
query: str,
port: str = "29418",
username: str | None = None,
extra_args: list[str] | None = None,
) -> list[dict[str, Any]]:
"""Execute a Gerrit SSH query and return parsed results.
Gerrit query returns newline-delimited JSON with a stats line at the end.
Args:
host: Gerrit server hostname.
query: Gerrit query string.
port: SSH port.
username: SSH username.
extra_args: Additional arguments (e.g., --current-patch-set).
Returns:
List of change/result dicts (stats line excluded).
Raises:
SystemExit: If SSH command fails.
"""
ssh_cmd = _get_ssh_cmd(host, port, username)
gerrit_args = ["gerrit", "query", "--format=JSON", query]
if extra_args:
gerrit_args.extend(extra_args)
cmd = [*ssh_cmd, *gerrit_args]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error: {result.stderr.strip()}", file=sys.stderr)
sys.exit(1)
results = []
for line in result.stdout.strip().splitlines():
if not line.strip():
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
# Skip the stats line (has "type": "stats")
if obj.get("type") == "stats":
continue
results.append(obj)
return results
# ============================================================================
# DATE FORMATTING
# ============================================================================
def format_timestamp(timestamp: int | None) -> str:
"""Format a Unix timestamp to YYYY-MM-DD HH:MM.
Args:
timestamp: Unix timestamp (seconds since epoch).
Returns:
Formatted date string, or "N/A" if input is None/0.
"""
if not timestamp:
return "N/A"
try:
dt = datetime.fromtimestamp(timestamp, tz=UTC)
return dt.strftime("%Y-%m-%d %H:%M")
except (OSError, ValueError, OverflowError):
return "N/A"
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def _get_owner(owner: dict[str, Any] | None) -> str:
"""Extract owner name from a Gerrit owner dict.
Args:
owner: Owner dictionary with 'username' or 'name' key.
Returns:
Username/name string or "Unknown".
"""
if not owner:
return "Unknown"
if isinstance(owner, dict):
return owner.get("username", owner.get("name", "Unknown"))
return str(owner)
# ============================================================================
# FORMAT FUNCTIONS — one per entity type (markdown output)
# ============================================================================
def format_change_summary(change: dict[str, Any]) -> str:
"""Format a Gerrit change for markdown display.
Args:
change: Change dictionary from Gerrit query JSON.
Returns:
Markdown-formatted string.
"""
number = change.get("number", "?")
subject = change.get("subject", "(No subject)")
status = change.get("status", "UNKNOWN")
owner = _get_owner(change.get("owner"))
project = change.get("project", "")
branch = change.get("branch", "")
created = format_timestamp(change.get("createdOn"))
lines = [
f"### Change {number}: {subject}",
f"- **Status:** {status}",
f"- **Owner:** {owner}",
]
if project:
lines.append(f"- **Project:** {project}")
if branch:
lines.append(f"- **Branch:** {branch}")
topic = change.get("topic")
if topic:
lines.append(f"- **Topic:** {topic}")
lines.append(f"- **Created:** {created}")
updated = format_timestamp(change.get("lastUpdated"))
if updated != "N/A":
lines.append(f"- **Updated:** {updated}")
# Current patch set approvals
patch_set = change.get("currentPatchSet", {})
if isinstance(patch_set, dict):
approvals = patch_set.get("approvals", [])
if approvals:
lines.append("\n**Approvals:**")
for approval in approvals:
if isinstance(approval, dict):
by = approval.get("by", {})
reviewer = (
by.get("username", by.get("name", "?")) if isinstance(by, dict) else "?"
)
a_type = approval.get("type", "?")
value = approval.get("value", "?")
lines.append(f"- **{a_type}:** {value} (by {reviewer})")
# Comments
comments = change.get("comments", [])
if comments:
lines.append(f"\n**Comments ({len(comments)}):**")
for comment in comments[-5:]: # Show last 5 comments
if isinstance(comment, dict):
reviewer = _get_owner(comment.get("reviewer"))
message = comment.get("message", "").strip()
ts = format_timestamp(comment.get("timestamp"))
if message:
# Truncate long messages
if len(message) > 200:
message = message[:200] + "..."
lines.append(f"- **{reviewer}** ({ts}): {message}")
url = change.get("url")
if url:
lines.append(f"\n- **URL:** {url}")
return "\n".join(lines)
def format_change_row(change: dict[str, Any]) -> str:
"""Format a Gerrit change as a compact markdown entry for lists.
Args:
change: Change dictionary from Gerrit query JSON.
Returns:
Markdown-formatted string.
"""
number = change.get("number", "?")
subject = change.get("subject", "(No subject)")
status = change.get("status", "UNKNOWN")
owner = _get_owner(change.get("owner"))
project = change.get("project", "")
created = format_timestamp(change.get("createdOn"))
lines = [
f"### Change {number}: {subject}",
f"- **Status:** {status}",
f"- **Owner:** {owner}",
]
if project:
lines.append(f"- **Project:** {project}")
lines.append(f"- **Created:** {created}")
return "\n".join(lines)
def format_project_row(project_name: str) -> str:
"""Format a Gerrit project name as a compact markdown entry.
Args:
project_name: Project name string.
Returns:
Markdown-formatted string.
"""
return f"### {project_name}"
# ============================================================================
# COMMAND HANDLERS — one per subcommand, return exit code
# ============================================================================
def cmd_check(args: argparse.Namespace) -> int:
"""Verify Gerrit SSH access is working.
Args:
args: Parsed arguments with host, port, username.
Returns:
Exit code (0 success, 1 error).
"""
gitreview = _read_gitreview()
host = args.host or gitreview["host"]
port = args.port or gitreview["port"]
username = args.username or gitreview.get("username") or None
if not host:
print(
"Error: No Gerrit host specified. Use --host or create a .gitreview file.",
file=sys.stderr,
)
return 1
ssh_cmd = _get_ssh_cmd(host, port, username)
cmd = [*ssh_cmd, "gerrit", "version"]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print("Error: Cannot connect to Gerrit.", file=sys.stderr)
print(result.stderr.strip(), file=sys.stderr)
print(f"\nCheck SSH access: ssh -p {port} {host} gerrit version", file=sys.stderr)
return 1
print("\u2713 Gerrit SSH access is working")
output = result.stdout.strip() or result.stderr.strip()
if output:
print(f" {output}")
return 0
def cmd_changes_list(args: argparse.Namespace) -> int:
"""List open changes.
Args:
args: Parsed arguments with host, port, username, project, limit, json flags.
Returns:
Exit code.
"""
gitreview = _read_gitreview()
host = args.host or gitreview["host"]
port = args.port or gitreview["port"]
username = args.username or gitreview.get("username") or None
project = gitreview.get("project", "")
if not host:
print("Error: No Gerrit host specified.", file=sys.stderr)
return 1
query = "status:open"
if project:
query += f" project:{project}"
query += f" limit:{args.limit}"
changes = run_gerrit_query(host, query, port, username)
if args.json:
print(json.dumps(changes, indent=2))
else:
if not changes:
print("No open changes found")
else:
print(f"## Open Changes\n\nFound {len(changes)} change(s):\n")
print("\n\n".join(format_change_row(c) for c in changes))
return 0
def cmd_changes_view(args: argparse.Namespace) -> int:
"""View a single change.
Args:
args: Parsed arguments with change number, host, port, username, json flags.
Returns:
Exit code.
"""
gitreview = _read_gitreview()
host = args.host or gitreview["host"]
port = args.port or gitreview["port"]
username = args.username or gitreview.get("username") or None
if not host:
print("Error: No Gerrit host specified.", file=sys.stderr)
return 1
changes = run_gerrit_query(
host,
f"change:{args.number}",
port,
username,
extra_args=["--current-patch-set", "--comments"],
)
if args.json:
print(json.dumps(changes, indent=2))
else:
if not changes:
print(f"Change {args.number} not found")
else:
print(format_change_summary(changes[0]))
return 0
def cmd_changes_search(args: argparse.Namespace) -> int:
"""Search changes with a custom query.
Args:
args: Parsed arguments with query, host, port, username, limit, json flags.
Returns:
Exit code.
"""
gitreview = _read_gitreview()
host = args.host or gitreview["host"]
port = args.port or gitreview["port"]
username = args.username or gitreview.get("username") or None
if not host:
print("Error: No Gerrit host specified.", file=sys.stderr)
return 1
query = f"{args.query} limit:{args.limit}"
changes = run_gerrit_query(host, query, port, username)
if args.json:
print(json.dumps(changes, indent=2))
else:
if not changes:
print("No changes found")
else:
print(f"## Search Results\n\nFound {len(changes)} change(s):\n")
print("\n\n".join(format_change_row(c) for c in changes))
return 0
def cmd_projects_list(args: argparse.Namespace) -> int:
"""List projects.
Args:
args: Parsed arguments with host, port, username, limit, json flags.
Returns:
Exit code.
"""
gitreview = _read_gitreview()
host = args.host or gitreview["host"]
port = args.port or gitreview["port"]
username = args.username or gitreview.get("username") or None
if not host:
print("Error: No Gerrit host specified.", file=sys.stderr)
return 1
ssh_cmd = _get_ssh_cmd(host, port, username)
cmd = [*ssh_cmd, "gerrit", "ls-projects", "--format", "json"]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error: {result.stderr.strip()}", file=sys.stderr)
sys.exit(1)
output = result.stdout.strip()
if not output:
if args.json:
print("{}")
else:
print("No projects found")
return 0
try:
projects_data = json.loads(output)
except json.JSONDecodeError:
# Fallback: treat as line-delimited project names
project_names = [line.strip() for line in output.splitlines() if line.strip()]
if args.json:
print(json.dumps(project_names, indent=2))
else:
if not project_names:
print("No projects found")
else:
items = project_names[: args.limit]
print(f"## Projects\n\nFound {len(items)} project(s):\n")
print("\n\n".join(format_project_row(p) for p in items))
return 0
if args.json:
print(json.dumps(projects_data, indent=2))
else:
# Gerrit ls-projects --format json returns {name: {id: ...}, ...}
project_names = sorted(projects_data.keys())[: args.limit]
if not project_names:
print("No projects found")
else:
print(f"## Projects\n\nFound {len(project_names)} project(s):\n")
print("\n\n".join(format_project_row(p) for p in project_names))
return 0
# ============================================================================
# ARGUMENT PARSER
# ============================================================================
def build_parser() -> argparse.ArgumentParser:
"""Build the argument parser with nested subcommands.
Returns:
Configured ArgumentParser.
"""
parser = argparse.ArgumentParser(
description="Gerrit wrapper for AI agents \u2014 markdown-formatted query output",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
# Global connection args
parser.add_argument("--host", help="Gerrit server hostname")
parser.add_argument("--port", default="", help="SSH port (default: 29418)")
parser.add_argument("--username", help="SSH username")
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
# check
subparsers.add_parser("check", help="Verify Gerrit SSH access")
# changes
changes_parser = subparsers.add_parser("changes", help="Change operations")
changes_sub = changes_parser.add_subparsers(dest="changes_command")
changes_list = changes_sub.add_parser("list", help="List open changes")
changes_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
changes_list.add_argument("--json", action="store_true", help="Output raw JSON")
changes_view = changes_sub.add_parser("view", help="View change details")
changes_view.add_argument("number", type=int, help="Change number")
changes_view.add_argument("--json", action="store_true", help="Output raw JSON")
changes_search = changes_sub.add_parser("search", help="Search changes")
changes_search.add_argument("query", help="Gerrit query string")
changes_search.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
changes_search.add_argument("--json", action="store_true", help="Output raw JSON")
# projects
projects_parser = subparsers.add_parser("projects", help="Project operations")
projects_sub = projects_parser.add_subparsers(dest="projects_command")
projects_list = projects_sub.add_parser("list", help="List projects")
projects_list.add_argument("--limit", type=int, default=30, help="Max results (default 30)")
projects_list.add_argument("--json", action="store_true", help="Output raw JSON")
return parser
# ============================================================================
# MAIN
# ============================================================================
def main() -> int:
"""Main entry point.
Returns:
Exit code.
"""
parser = build_parser()
args = parser.parse_args()
if not args.command:
parser.print_help()
return 1
if args.command == "check":
return cmd_check(args)
elif args.command == "changes":
if not hasattr(args, "changes_command") or not args.changes_command:
parser.parse_args(["changes", "--help"])
return 1
if args.changes_command == "list":
return cmd_changes_list(args)
elif args.changes_command == "view":
return cmd_changes_view(args)
elif args.changes_command == "search":
return cmd_changes_search(args)
elif args.command == "projects":
if not hasattr(args, "projects_command") or not args.projects_command:
parser.parse_args(["projects", "--help"])
return 1
if args.projects_command == "list":
return cmd_projects_list(args)
parser.print_help()
return 1
if __name__ == "__main__":
sys.exit(main())