Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- `repo get` table output is a curated key/value detail view instead of the list summary columns.
- Split output formatting into projection helpers and generic renderers (`format_repo_get` / `format_repo_list`).

### Documentation

- Clarified `repo get` vs `repo list` table/JSON field behavior in the CLI guide.

## [2.0.0] - 2026-07-21

### Changed
Expand Down
9 changes: 8 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ github-rest-cli repo get --name my-repo --format json
| `-o` / `--org` | No | authenticated user | Organization owner |
| `-f` / `--format` | No | `table` | Output format: `table` or `json` |

Table mode shows a key/value detail view (`Field` | `Value`) with curated fields: `name`, `full_name`, `owner`, `description`, `visibility`, `default_branch`, `language`, `topics`, `html_url`, `created_at`, `updated_at`, `pushed_at`, `fork`, `archived`, `disabled`.

JSON mode returns the full raw GitHub repository object from the API.

### `repo list`

List repositories for the authenticated user.
Expand All @@ -69,7 +73,7 @@ github-rest-cli repo list --role owner --format json
| `-r` / `--role` | No | unset | Filter by affiliation/role |
| `-f` / `--format` | No | `table` | Output format: `table` or `json` |

`--format` only changes presentation. Table and JSON use the same repository set and the same fields: `name`, `owner`, `url`, `visibility`.
`--format` only changes presentation. Table and JSON use the same repository set and the same summary fields: `name`, `owner`, `url`, `visibility`.

### `repo create`

Expand Down Expand Up @@ -152,6 +156,9 @@ github-rest-cli environment create --name my-repo --env staging --org my-org
- `table` (default) — PrettyTable display
- `json` — JSON string suitable for piping or scripting

For `repo get`, table is a curated key/value detail view; JSON is the full API payload.
For `repo list`, table and JSON both use the summary fields `name`, `owner`, `url`, `visibility`.

```shell
github-rest-cli repo list --format json
github-rest-cli repo get --name my-repo --format table
Expand Down
16 changes: 3 additions & 13 deletions src/github_rest_cli/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import requests
from github_rest_cli.globals import get_api_url, get_headers
from github_rest_cli.utils import rich_output, CliOutput
from github_rest_cli.utils import rich_output, format_repo_get, format_repo_list


def request_with_handling(
Expand Down Expand Up @@ -68,12 +68,7 @@ def get_repository(name: str, org: str = None, output_format: str = "table"):
if not response:
return None

data = response.json()

if output_format == "json":
return CliOutput(data).get_json_output()

return CliOutput([data]).default_format()
return format_repo_get(response.json(), output_format)


def list_repositories(page: int, property: str, role: str, output_format: str):
Expand All @@ -93,12 +88,7 @@ def list_repositories(page: int, property: str, role: str, output_format: str):
if not response:
return None

output = CliOutput(response.json())

if output_format == "json":
return output.json_format()

return output.default_format()
return format_repo_list(response.json(), output_format)


def create_repository(name: str, visibility: str, org: str = None, empty: bool = False):
Expand Down
163 changes: 99 additions & 64 deletions src/github_rest_cli/utils.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,105 @@
from rich import print as rprint
import json


class CliFormatOutput:
def to_json(self, data):
return json.dumps(data, indent=2)

def to_table(self, fields, data):
from prettytable import PrettyTable

table = PrettyTable()
table.title = "GitHub Repositories"
table.header_style = "upper"

if isinstance(fields, list):
table.field_names = fields

# Align all columns to left.
table.align = "l"

if isinstance(data, list):
for row in data:
table.add_row(row)

return table


class CliOutput:
def __init__(self, data):
self.data = data
self.formatter = CliFormatOutput()

def json_format(self):
repos = {
"repositories": [
{
"name": f.get("name"),
"owner": f.get("owner", {}).get("login"),
"url": f.get("html_url"),
"visibility": f.get("visibility"),
}
for f in self.data
],
}
return self.formatter.to_json(repos)

def table_format(self):
fields = ["name", "owner", "url", "visibility"]
values = []
for repo in self.data:
values.append(
[
repo.get("name"),
repo.get("owner", {}).get("login"),
repo.get("html_url"),
repo.get("visibility"),
]
)

return self.formatter.to_table(fields, values)

def get_json_output(self):
return self.formatter.to_json(self.data)

def default_format(self):
return self.table_format()
REPO_SUMMARY_COLUMNS = ["name", "owner", "url", "visibility"]

REPO_DETAIL_FIELDS = [
"name",
"full_name",
"owner",
"description",
"visibility",
"default_branch",
"language",
"topics",
"html_url",
"created_at",
"updated_at",
"pushed_at",
"fork",
"archived",
"disabled",
]


def to_json(data) -> str:
return json.dumps(data, indent=2)


def to_table(rows, *, columns, title):
from prettytable import PrettyTable

table = PrettyTable()
table.title = title
table.header_style = "upper"
table.field_names = columns
table.align = "l"

for row in rows:
table.add_row(row)

return table


def to_key_value_table(pairs, *, title):
return to_table(pairs, columns=["field", "value"], title=title)


def _stringify(value) -> str:
if value is None:
return ""
if isinstance(value, bool):
return "true" if value else "false"
return str(value)


def project_repo_summary(repo: dict) -> dict:
return {
"name": repo.get("name"),
"owner": repo.get("owner", {}).get("login"),
"url": repo.get("html_url"),
"visibility": repo.get("visibility"),
}


def project_repo_detail(repo: dict) -> list[tuple[str, str]]:
topics = repo.get("topics") or []
values = {
"name": repo.get("name"),
"full_name": repo.get("full_name"),
"owner": repo.get("owner", {}).get("login"),
"description": repo.get("description") or "",
"visibility": repo.get("visibility"),
"default_branch": repo.get("default_branch"),
"language": repo.get("language"),
"topics": ", ".join(topics),
"html_url": repo.get("html_url"),
"created_at": repo.get("created_at"),
"updated_at": repo.get("updated_at"),
"pushed_at": repo.get("pushed_at"),
"fork": repo.get("fork"),
"archived": repo.get("archived"),
"disabled": repo.get("disabled"),
}
return [(field, _stringify(values[field])) for field in REPO_DETAIL_FIELDS]


def format_repo_list(repos, output_format: str = "table"):
summaries = [project_repo_summary(repo) for repo in repos]

if output_format == "json":
return to_json({"repositories": summaries})

rows = [[s[column] for column in REPO_SUMMARY_COLUMNS] for s in summaries]
return to_table(rows, columns=REPO_SUMMARY_COLUMNS, title="GitHub Repositories")


def format_repo_get(repo, output_format: str = "table"):
if output_format == "json":
return to_json(repo)

pairs = project_repo_detail(repo)
return to_key_value_table(pairs, title="GitHub Repository")


def rich_output(message: str, format_str: str = "bold green"):
Expand Down
22 changes: 21 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,20 @@ def test_create_repository_org(mocker):

SAMPLE_REPO = {
"name": "test-repo",
"owner": {"login": "test-user"},
"full_name": "test-user/test-repo",
"owner": {"login": "test-user", "id": 1},
"description": "A test repository",
"html_url": "https://github.com/test-user/test-repo",
"visibility": "public",
"default_branch": "main",
"language": "Python",
"topics": ["cli", "github"],
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-06-01T00:00:00Z",
"pushed_at": "2024-06-02T00:00:00Z",
"fork": False,
"archived": False,
"disabled": False,
}


Expand All @@ -70,6 +81,8 @@ def test_get_repository_json_format(mocker):
assert isinstance(result, str)
assert '"name": "test-repo"' in result
assert '"visibility": "public"' in result
assert '"login": "test-user"' in result
assert '"id": 1' in result


def test_get_repository_table_format(mocker):
Expand All @@ -78,8 +91,13 @@ def test_get_repository_table_format(mocker):
result = api.get_repository("test-repo", output_format="table")

table_text = str(result)
assert "GITHUB REPOSITORY" in table_text.upper()
assert "FIELD" in table_text.upper()
assert "VALUE" in table_text.upper()
assert "test-repo" in table_text
assert "test-user" in table_text
assert "default_branch" in table_text
assert "main" in table_text
assert not table_text.strip().startswith("{")


Expand All @@ -91,6 +109,8 @@ def test_list_repositories_json_format(mocker):
assert isinstance(result, str)
assert '"repositories"' in result
assert '"name": "test-repo"' in result
assert '"owner": "test-user"' in result
assert '"login"' not in result


def test_list_repositories_table_format(mocker):
Expand Down
Loading
Loading