Coverage for src/workstack/cli/commands/gt.py: 46%
67 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
1"""Graphite integration commands for workstack.
3Provides machine-readable access to Graphite metadata for scripting and automation.
4"""
6import json
7from dataclasses import asdict
8from pathlib import Path
10import click
12from workstack.cli.core import discover_repo_context
13from workstack.core.branch_metadata import BranchMetadata
14from workstack.core.context import WorkstackContext
15from workstack.core.gitops import GitOps
18@click.group("graphite")
19@click.pass_obj
20def graphite_group(ctx: WorkstackContext) -> None:
21 """Graphite integration commands for machine-readable metadata.
23 Requires use-graphite enabled.
24 """
25 pass
28@graphite_group.command("branches")
29@click.option(
30 "--format",
31 type=click.Choice(["text", "json", "tree"]),
32 default="text",
33 help="Output format (text, json, or tree)",
34)
35@click.option(
36 "--stack",
37 type=str,
38 default=None,
39 help="Show only this branch and its descendants (tree format only)",
40)
41@click.pass_obj
42def graphite_branches_cmd(ctx: WorkstackContext, format: str, stack: str | None) -> None:
43 """List all Graphite-tracked branches with machine-readable metadata.
45 By default, outputs a simple list of branch names (one per line).
46 Use --format json for structured output with full metadata.
47 Use --format tree for hierarchical tree visualization.
49 Examples:
50 $ workstack graphite branches
51 main
52 feature-1
53 feature-2
55 $ workstack graphite branches --format json
56 {
57 "branches": [
58 {
59 "name": "main",
60 "parent": null,
61 "children": ["feature-1"],
62 "is_trunk": true,
63 "commit_sha": "abc123..."
64 }
65 ]
66 }
68 $ workstack graphite branches --format tree
69 main (abc123f) "Initial commit"
70 ├─ feature-a (def456g) "Add user authentication"
71 │ └─ feature-a-tests (789hij0) "Add tests for auth"
72 └─ feature-b (klm123n) "Refactor database layer"
74 $ workstack graphite branches --format tree --stack feature-a
75 feature-a (def456g) "Add user authentication"
76 └─ feature-a-tests (789hij0) "Add tests for auth"
78 Requires:
79 - Graphite enabled (use_graphite config)
80 - Valid .git/.graphite_cache_persist file
81 """
82 # Check if graphite is enabled
83 if not ctx.global_config_ops.get_use_graphite():
84 click.echo(
85 "Error: Graphite not enabled. Run 'workstack config set use_graphite true'",
86 err=True,
87 )
88 raise SystemExit(1)
90 # Check if --stack is used without tree format
91 if stack is not None and format != "tree":
92 click.echo(
93 "Error: --stack option can only be used with --format tree",
94 err=True,
95 )
96 raise SystemExit(1)
98 # Discover repository
99 repo = discover_repo_context(ctx, Path.cwd())
101 # Get branches from GraphiteOps
102 branches_dict = ctx.graphite_ops.get_all_branches(ctx.git_ops, repo.root)
104 if format == "json":
105 # Convert to list of dicts for JSON output
106 branches_list = [asdict(metadata) for metadata in branches_dict.values()]
107 output = {"branches": branches_list}
108 click.echo(json.dumps(output, indent=2))
109 elif format == "tree":
110 # Tree format: hierarchical display with commit info
111 output = _format_branches_as_tree(branches_dict, ctx.git_ops, repo.root, root_branch=stack)
112 click.echo(output)
113 else:
114 # Text format: simple list of branch names
115 for branch_name in sorted(branches_dict.keys()):
116 click.echo(branch_name)
119def _format_branches_as_tree(
120 branches: dict[str, BranchMetadata],
121 git_ops: GitOps,
122 repo_root: Path,
123 *,
124 root_branch: str | None,
125) -> str:
126 """Format branches as a hierarchical tree.
128 Args:
129 branches: Mapping of branch name to metadata
130 git_ops: GitOps instance for retrieving commit messages
131 repo_root: Repository root path
132 root_branch: Optional branch to use as root (shows only this branch and descendants)
134 Returns:
135 Multi-line string with tree visualization
136 """
137 # Determine which branches to show as roots
138 if root_branch is not None:
139 # Filter to specific branch and its descendants
140 if root_branch not in branches:
141 return f"Error: Branch '{root_branch}' not found"
142 roots = [root_branch]
143 else:
144 # Show all trunk branches (branches with no parent)
145 roots = [name for name, meta in branches.items() if meta.is_trunk]
147 if not roots:
148 return "No branches found"
150 # Build tree lines
151 lines: list[str] = []
152 for i, root in enumerate(roots):
153 is_last_root = i == len(roots) - 1
154 _format_branch_recursive(
155 branch_name=root,
156 branches=branches,
157 git_ops=git_ops,
158 repo_root=repo_root,
159 lines=lines,
160 prefix="",
161 is_last=is_last_root,
162 is_root=True,
163 )
165 return "\n".join(lines)
168def _format_branch_recursive(
169 branch_name: str,
170 branches: dict[str, BranchMetadata],
171 git_ops: GitOps,
172 repo_root: Path,
173 lines: list[str],
174 prefix: str,
175 is_last: bool,
176 is_root: bool,
177) -> None:
178 """Recursively format a branch and its children.
180 Args:
181 branch_name: Name of current branch to format
182 branches: All branches metadata
183 git_ops: GitOps instance for retrieving commit messages
184 repo_root: Repository root path
185 lines: List to append formatted lines to
186 prefix: Prefix string for indentation
187 is_last: True if this is the last child of its parent
188 is_root: True if this is a root node
189 """
190 if branch_name not in branches:
191 return
193 metadata = branches[branch_name]
195 # Get commit info
196 short_sha = metadata.commit_sha[:7] if metadata.commit_sha else "unknown"
197 commit_message = (
198 git_ops.get_commit_message(repo_root, metadata.commit_sha) or "No commit message"
199 )
201 # Format current line
202 connector = "└─" if is_last else "├─"
203 branch_info = f'{branch_name} ({short_sha}) "{commit_message}"'
205 if is_root:
206 # Root node: no connector
207 line = branch_info
208 else:
209 # All other nodes get connectors
210 line = f"{prefix}{connector} {branch_info}"
212 lines.append(line)
214 # Process children
215 children = metadata.children
216 if children:
217 # Determine prefix for children
218 if prefix:
219 # Non-root node: extend existing prefix
220 child_prefix = prefix + (" " if is_last else "│ ")
221 else:
222 # Root node's children: start with appropriate spacing
223 child_prefix = " " if is_last else "│ "
225 for i, child in enumerate(children):
226 is_last_child = i == len(children) - 1
227 _format_branch_recursive(
228 branch_name=child,
229 branches=branches,
230 git_ops=git_ops,
231 repo_root=repo_root,
232 lines=lines,
233 prefix=child_prefix,
234 is_last=is_last_child,
235 is_root=False,
236 )