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
8 changes: 7 additions & 1 deletion phabfive/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ def preprocess_monograms(argv: list[str]) -> list[str]:
after = argv[monogram_idx + 1 :]

# Handle comment shortcut: T123 'text' → maniphest comment T123 'text'
if prefix in _COMMENT_PREFIXES and after and not after[0].startswith("-"):
# But not when the next arg is also a monogram (T123 T456 → show both)
if (
prefix in _COMMENT_PREFIXES
and after
and not after[0].startswith("-")
and not _MONOGRAM_PATTERN.match(after[0])
):
app_name = expansion[0] # e.g., 'maniphest'
return before + [app_name, "comment", monogram] + after

Expand Down
46 changes: 25 additions & 21 deletions phabfive/cli/maniphest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ def _setup_output_options(ctx: typer.Context):
def _display_tasks(result, output_format, maniphest_instance):
"""Display task search/show results in the specified format."""
from phabfive.display import (
display_task_json,
display_task_rich,
display_task_yaml,
display_task_tree,
display_tasks_json,
display_tasks_rich,
display_tasks_tree,
display_tasks_yaml,
)

if not result or not result.get("tasks"):
Expand All @@ -70,15 +70,15 @@ def _display_tasks(result, output_format, maniphest_instance):
console = maniphest_instance.get_console()

try:
for task_dict in result["tasks"]:
if output_format == "tree":
display_task_tree(console, task_dict, maniphest_instance)
elif output_format in ("yaml", "strict"):
display_task_yaml(task_dict)
elif output_format == "json":
display_task_json(task_dict)
else: # "rich" (default)
display_task_rich(console, task_dict, maniphest_instance)
tasks = result["tasks"]
if output_format == "json":
display_tasks_json(tasks)
elif output_format == "tree":
display_tasks_tree(console, tasks, maniphest_instance)
elif output_format in ("yaml", "strict"):
display_tasks_yaml(tasks)
else: # "rich" (default)
display_tasks_rich(console, tasks, maniphest_instance)
except BrokenPipeError:
sys.stderr.close()
sys.exit(0)
Expand All @@ -87,7 +87,7 @@ def _display_tasks(result, output_format, maniphest_instance):
@maniphest_app.command()
def show(
ctx: typer.Context,
ticket_id: str = typer.Argument(..., help="Task ID (e.g., T123)"),
ticket_ids: List[str] = typer.Argument(..., help="Task ID(s) (e.g., T123 T456)"),
show_history: bool = typer.Option(
False, "--show-history", "-H", help="Display transition history"
),
Expand All @@ -98,19 +98,23 @@ def show(
False, "--show-comments", "-C", help="Display comments on the task"
),
) -> None:
"""Show details for a Maniphest task."""
"""Show details for one or more Maniphest tasks."""
_setup_output_options(ctx)
maniphest = _get_maniphest_app()

# Validate ticket ID format
# Validate all ticket ID formats
maniphest_pattern = f"^{MONOGRAMS['maniphest']}$"
if not re.match(maniphest_pattern, ticket_id):
typer.echo(f"Invalid task ID '{ticket_id}'. Expected format: T123", err=True)
raise typer.Exit(1)
task_ids = []
for ticket_id in ticket_ids:
if not re.match(maniphest_pattern, ticket_id):
typer.echo(
f"Invalid task ID '{ticket_id}'. Expected format: T123", err=True
)
raise typer.Exit(1)
task_ids.append(int(ticket_id[1:]))

task_id = int(ticket_id[1:])
result = maniphest.task_show(
task_id,
task_ids,
show_history=show_history,
show_metadata=show_metadata,
show_comments=show_comments,
Expand Down
105 changes: 77 additions & 28 deletions phabfive/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _needs_yaml_quoting(value):
return value == "" or any(c in value for c in ":{}[]`'\"")


def display_task_rich(console, task_dict, phabfive_instance):
def _display_task_rich(console, task_dict, phabfive_instance):
"""Display a single task in YAML-like format using Rich.

Parameters
Expand Down Expand Up @@ -197,7 +197,7 @@ def display_task_rich(console, task_dict, phabfive_instance):
console.print(f" {meta_key}: {_escape_for_rich(meta_value)}")


def display_task_tree(console, task_dict, phabfive_instance):
def _display_task_tree(console, task_dict, phabfive_instance):
"""Display a single task in tree format using Rich Tree.

Parameters
Expand Down Expand Up @@ -335,17 +335,8 @@ def display_task_tree(console, task_dict, phabfive_instance):
console.print(tree)


def display_task_yaml(task_dict):
"""Display task as strict YAML via ruamel.yaml.

Guaranteed conformant YAML output for piping to yq/jq.
No hyperlinks, no Rich formatting.

Parameters
----------
task_dict : dict
Task data dictionary with Link, Task, Boards, History, Metadata, etc.
"""
def _display_task_yaml(task_dict):
"""Display a single task as strict YAML via ruamel.yaml."""
yaml = YAML()
yaml.default_flow_style = False

Expand Down Expand Up @@ -414,16 +405,18 @@ def display_task_yaml(task_dict):
print(stream.getvalue(), end="")


def display_task_json(task_dict):
"""Display task as JSON.

Machine-readable JSON output for piping to jq or other tools.
No hyperlinks, no Rich formatting.
def _build_task_json_output(task_dict):
"""Build a clean JSON-serializable dict from a task_dict.

Parameters
----------
task_dict : dict
Task data dictionary with Link, Task, Boards, History, Metadata, etc.

Returns
-------
dict
Clean dictionary ready for JSON serialization.
"""
# Build clean dict - use _url for the Link (plain URL string)
output = {"Link": task_dict.get("_url", "")}
Expand Down Expand Up @@ -491,7 +484,63 @@ def display_task_json(task_dict):
if task_dict.get("Metadata"):
output["Metadata"] = task_dict["Metadata"]

print(json.dumps(output, indent=2))
return output


def display_tasks_rich(console, task_dicts, phabfive_instance):
"""Display tasks in YAML-like format using Rich.

Parameters
----------
console : Console
Rich Console instance for output
task_dicts : list[dict]
List of task data dictionaries.
phabfive_instance : Phabfive
Instance to access format_link() and url
"""
for task_dict in task_dicts:
_display_task_rich(console, task_dict, phabfive_instance)


def display_tasks_tree(console, task_dicts, phabfive_instance):
"""Display tasks in tree format using Rich Tree.

Parameters
----------
console : Console
Rich Console instance for output
task_dicts : list[dict]
List of task data dictionaries.
phabfive_instance : Phabfive
Instance to access format_link() and url
"""
for task_dict in task_dicts:
_display_task_tree(console, task_dict, phabfive_instance)


def display_tasks_yaml(task_dicts):
"""Display tasks as strict YAML.

Parameters
----------
task_dicts : list[dict]
List of task data dictionaries.
"""
for task_dict in task_dicts:
_display_task_yaml(task_dict)


def display_tasks_json(task_dicts):
"""Display tasks as a valid JSON array.

Parameters
----------
task_dicts : list[dict]
List of task data dictionaries.
"""
outputs = [_build_task_json_output(td) for td in task_dicts]
print(json.dumps(outputs, indent=2))


def display_tasks(result, output_format, phabfive_instance):
Expand All @@ -512,15 +561,15 @@ def display_tasks(result, output_format, phabfive_instance):
console = phabfive_instance.get_console()

try:
for task_dict in result["tasks"]:
if output_format == "tree":
display_task_tree(console, task_dict, phabfive_instance)
elif output_format in ("yaml", "strict"):
display_task_yaml(task_dict)
elif output_format == "json":
display_task_json(task_dict)
else: # "rich" (default)
display_task_rich(console, task_dict, phabfive_instance)
tasks = result["tasks"]
if output_format == "json":
display_tasks_json(tasks)
elif output_format == "tree":
display_tasks_tree(console, tasks, phabfive_instance)
elif output_format in ("yaml", "strict"):
display_tasks_yaml(tasks)
else: # "rich" (default)
display_tasks_rich(console, tasks, phabfive_instance)
except BrokenPipeError:
# Handle pipe closed by consumer (e.g., head, less)
# Quietly exit - this is normal behavior
Expand Down
Loading
Loading