# 0001 — Split single-file monolith into multi-module package

## Problem

Everything lives in `jira_git_helper.py` (~3,700 lines). This makes it hard to:
- Navigate and reason about the code
- Work on one area without scrolling past unrelated code
- Identify shared patterns vs. duplicated code

## Proposed structure

```
jira_git_helper/
├── __init__.py           # __version__, re-export `main` for entry point
├── config.py             # Config file + ticket state management
├── jira_api.py           # JIRA client, field cache, issue fetching, PR helpers
├── git.py                # All git subprocess wrappers
├── formatters.py         # Formatter config, execution, eof fixer
├── tui/
│   ├── __init__.py
│   ├── theme.py          # Shared CSS blocks, colour constants, ticket info renderer
│   ├── modals.py         # TextInputModal, ConfirmModal, FmtModal
│   ├── ticket_picker.py  # JiraListApp, FieldPickerModal, FilterListModal, TicketInfoModal
│   ├── branch.py         # BranchPromptApp, BranchPickerApp, BranchDiffModal
│   ├── file_picker.py    # FilePickerApp, CommitModal
│   ├── pr_picker.py      # PrPickerApp, DiffModal
│   └── prune.py          # PruneApp
└── cli.py                # Click group + all command functions
```

Delete `jira_git_helper.py` once the migration is complete.

## Module responsibilities

### `config.py`
All config and ticket state management. No JIRA or git imports.

**Functions to move:**
- `get_ticket`, `save_ticket`, `clear_ticket`
- `_read_config`, `_write_config`, `get_config`, `set_config`
- `get_projects`, `get_fields_for_project`
- `get_filters_for_project`, `set_filters_for_project`
- `get_active_filter_name`, `set_active_filter_name`
- `get_effective_filter_name`, `get_jql_for_project`
- `get_formatters`, `set_formatters`

**Module-level state to move:**
- `STATE_FILE`, `CONFIG_FILE`, `_FALLBACK_JQL`
- `_session_active_filters`

### `jira_api.py`
JIRA client setup, field caching, issue/PR fetching. Depends on `config.py`.

**Functions to move:**
- `get_jira_server`, `get_jira_client`
- `_ensure_fields_cached`, `_get_jira_field_id`, `_get_jira_field_name`
- `fetch_issues_for_projects`
- `_get_prs`

**Module-level state to move:**
- `_field_id_by_name`, `_field_name_by_id`
- `STATUS_STYLES`, `PRIORITY_STYLES`, `PR_STATUS_STYLES`

### `git.py`
Pure git subprocess wrappers. No JIRA or TUI imports.

**Functions to move:**
- `_get_file_statuses`
- `_get_current_branch`
- `_check_not_main_branch`
- `_get_default_branch`
- `_get_local_branches`
- `_copy_to_clipboard`

### `formatters.py`
Formatter execution logic. Depends on `config.py` and `git.py`.

**Functions to move:**
- `_get_binary_paths`
- `_fix_eof`
- `_build_fmt_table`
- `_run_formatters`
- `FILE_STATUS_LABELS`

### `tui/theme.py`
Shared CSS and rendering helpers. This is where we **eliminate duplication**.

**Shared CSS constants:**
```python
SCREEN_CSS       # Screen { background: #0a0e0a; }
CONTEXT_BAR_CSS  # .context-bar { ... }
DATATABLE_CSS    # DataTable + header/cursor/hover/odd/even rows
FOOTER_CSS       # Footer + .footer--key
FILTER_BAR_CSS   # #filter-bar { ... }
MODAL_CSS        # Common modal align/background/border
```

Each TUI app composes its CSS from these blocks:
```python
CSS = SCREEN_CSS + CONTEXT_BAR_CSS + DATATABLE_CSS + FOOTER_CSS + FILTER_BAR_CSS + """
    /* app-specific overrides */
"""
```

**Colour constants:**
```python
COL_GREEN   = "#00ff41"
COL_CYAN    = "#00e5ff"
COL_PALE    = "#b8d4b8"
COL_AMBER   = "#ffb300"
COL_PURPLE  = "#b39ddb"
COL_RED     = "#ff5555"
COL_BG      = "#0a0e0a"
COL_SURFACE = "#0d1a0d"
COL_DARK    = "#152015"
```

**Shared ticket info renderer (eliminates duplication between TicketInfoModal and BranchPromptApp):**
```python
def build_ticket_info(issue, jira_server: str) -> Group:
    """Build a Rich renderable with ticket summary, meta table, URL, and description."""
    ...
```

**Shared context bar builder:**
```python
def context_bar_text() -> str:
    """Format context bar showing active ticket + current branch."""
    ...
```

### `tui/modals.py`
Generic reusable modals. No JIRA or git imports.

**Classes to move:**
- `TextInputModal`
- `ConfirmModal`
- `FmtModal`

### `tui/ticket_picker.py`
The `jg set` screen and its supporting modals. Depends on `jira_api.py`, `config.py`, `tui/theme.py`.

**Classes to move:**
- `JiraListApp`
- `FieldPickerModal`
- `FilterListModal`
- `TicketInfoModal`

**Shared logic to extract:**
- `ensure_ticket()` moves here (it creates `JiraListApp`)

### `tui/branch.py`
Branch-related TUI apps. Depends on `jira_api.py`, `tui/theme.py`.

**Classes to move:**
- `BranchPromptApp`
- `BranchPickerApp`
- `BranchDiffModal`

### `tui/file_picker.py`
File staging TUI. Depends on `git.py`, `tui/theme.py`, `tui/modals.py`.

**Classes to move:**
- `FilePickerApp`
- `CommitModal`

### `tui/pr_picker.py`
PR browser. Depends on `jira_api.py`, `tui/theme.py`.

**Classes to move:**
- `PrPickerApp`
- `DiffModal`

### `tui/prune.py`
Branch pruning. Depends on `git.py`, `tui/theme.py`, `tui/modals.py`.

**Classes to move:**
- `PruneApp`

### `cli.py`
All Click command functions. Thin orchestration — imports from the other modules and wires things together.

**Move here:**
- `main` Click group
- All `@main.command()` and `@main.group()` functions
- `cmd_set`, `cmd_clear`, `cmd_version`, `cmd_branch`, `cmd_add`, `cmd_commit`
- `cmd_push`, `cmd_reset`, `cmd_sync`, `cmd_prune`, `cmd_prs`
- `cmd_fmt`, `cmd_fmt_add`, `cmd_fmt_list`, `cmd_fmt_delete`
- `cmd_info`, `cmd_debug`, `cmd_open`
- `cmd_config`, `config_get`, `config_set`, `config_list`
- `cmd_hook`, `cmd_setup`

### `__init__.py`
```python
from importlib.metadata import version, PackageNotFoundError
try:
    __version__ = version("jira-git-helper")
except PackageNotFoundError:
    __version__ = "unknown"

from .cli import main

__all__ = ["main", "__version__"]
```

## Key deduplication

| Duplication | Current | Fix |
|---|---|---|
| DataTable CSS (5 copies) | Inline in each App class | `theme.DATATABLE_CSS` constant |
| Footer CSS (12+ copies) | Inline in each class | `theme.FOOTER_CSS` constant |
| Context bar CSS (4 copies) | Inline in each App class | `theme.CONTEXT_BAR_CSS` constant |
| Filter bar CSS (3 copies) | Inline in each App class | `theme.FILTER_BAR_CSS` constant |
| Ticket info fetch (2 copies) | `TicketInfoModal._fetch_info`, `BranchPromptApp._fetch_info` | `theme.build_ticket_info()` |
| Context bar text (1 fn, imported everywhere) | `_context_bar_text()` | `theme.context_bar_text()` |

## Entry point update

`pyproject.toml` changes from:
```toml
[project.scripts]
jg = "jira_git_helper:main"
```
to:
```toml
[project.scripts]
jg = "jira_git_helper:main"
```

No change needed — `__init__.py` re-exports `main` from `cli.py`, so the entry point stays the same.

## Implementation order

1. **Create package skeleton** — directories, empty `__init__.py` files
2. **Extract `config.py`** — pure functions, no other local deps, easy to test in isolation
3. **Extract `jira_api.py`** — depends only on `config.py`
4. **Extract `git.py`** — no local deps
5. **Extract `formatters.py`** — depends on `config.py` + `git.py`
6. **Extract `tui/theme.py`** — CSS constants, colour constants, `build_ticket_info()`, `context_bar_text()`
7. **Extract `tui/modals.py`** — generic modals (no JIRA/git deps)
8. **Extract TUI apps** — `ticket_picker.py`, `branch.py`, `file_picker.py`, `pr_picker.py`, `prune.py`
9. **Extract `cli.py`** — all Click commands
10. **Delete `jira_git_helper.py`** — verify `uv tool install --reinstall .` works
11. **Smoke test** — run `jg set`, `jg add`, `jg branch`, `jg prs`, `jg prune`, `jg fmt`, `jg config list`

## Risks

- **Circular imports**: `ensure_ticket()` creates a `JiraListApp` and calls `save_ticket()`. Keep `ensure_ticket` in `tui/ticket_picker.py` (it's inherently TUI-coupled) and import config functions.
- **Module-level state**: `_session_active_filters` and field caches are module singletons. They stay in their respective modules and work as before since Python caches module objects.
- **Click group wiring**: all `@main.command()` decorators must execute at import time. `cli.py` imports all command functions and registers them.

## Acceptance criteria

- `uv tool install --reinstall .` succeeds
- All commands work identically to before
- No functionality changes — pure structural refactor
- `jira_git_helper.py` is deleted
- Each module is <500 lines (ideally <300)

---

## Status: DONE

Completed 2026-02-24.

## Summary of actions taken

### 1. Created package skeleton
- Created `jira_git_helper/` directory with `__init__.py` and `tui/` subdirectory with its own `__init__.py`.

### 2. Extracted core modules (no TUI dependencies)
- **`config.py`** (150 lines) — All config/state management: `STATE_FILE`, `CONFIG_FILE`, `_FALLBACK_JQL`, `_session_active_filters`, and 16 functions (`get_ticket`, `save_ticket`, `clear_ticket`, `get_config`, `set_config`, `get_projects`, `get_fields_for_project`, `get_filters_for_project`, `set_filters_for_project`, `get_active_filter_name`, `set_active_filter_name`, `get_formatters`, `set_formatters`, `get_effective_filter_name`, `get_jql_for_project`).
- **`git.py`** (97 lines) — Git subprocess wrappers: `get_file_statuses`, `get_current_branch`, `check_not_main_branch`, `get_default_branch`, `get_local_branches`, `copy_to_clipboard`. Removed leading underscores to make public API.
- **`jira_api.py`** (172 lines) — JIRA client setup, field caching, issue/PR fetching: `get_jira_server`, `get_jira_client`, `ensure_fields_cached`, `get_jira_field_id`, `get_jira_field_name`, `fetch_issues_for_projects`, `get_prs`, `get_default_jql`. Includes `STATUS_STYLES`, `PRIORITY_STYLES`, `PR_STATUS_STYLES`.
- **`formatters.py`** (153 lines) — `FILE_STATUS_LABELS`, `get_binary_paths`, `fix_eof`, `build_fmt_table`, `run_formatters`.

### 3. Extracted TUI modules
- **`tui/theme.py`** (172 lines) — Shared CSS blocks (`SCREEN_CSS`, `CONTEXT_BAR_CSS`, `DATATABLE_CSS`, `FOOTER_CSS`, `FILTER_BAR_CSS`, `MODAL_CSS`), colour constants, `context_bar_text()`, `build_ticket_info()`, `preview_raw_value()`. Eliminated CSS duplication across all TUI apps.
- **`tui/modals.py`** (143 lines) — Generic reusable modals: `TextInputModal`, `ConfirmModal`, `FmtModal`.
- **`tui/ticket_picker.py`** (829 lines) — `JiraListApp`, `FieldPickerModal`, `FilterListModal`, `TicketInfoModal`, `ensure_ticket()`.
- **`tui/branch.py`** (274 lines) — `BranchPromptApp`, `BranchPickerApp`, `BranchDiffModal`.
- **`tui/file_picker.py`** (418 lines) — `FilePickerApp`, `CommitModal`.
- **`tui/pr_picker.py`** (379 lines) — `PrPickerApp`, `DiffModal` (with search, file navigation, delta/syntax highlighting).
- **`tui/prune.py`** (168 lines) — `PruneApp` with branch selection, deletion, diff viewing.

### 4. Extracted CLI
- **`cli.py`** (879 lines) — All Click commands with lazy TUI imports inside command functions to avoid circular imports and speed up CLI startup.

### 5. Updated entry point
- Changed `pyproject.toml` from `jg = "jira_git_helper:main"` to `jg = "jira_git_helper.cli:main"`.

### 6. Deleted monolith
- Removed the original `jira_git_helper.py` (~3,736 lines).

### 7. Verification
- All module imports pass (`uv run python3 -c "from jira_git_helper.cli import main"` etc.)
- `uv tool install --reinstall .` succeeds
- `jg version` returns `0.19.0`

### Final file sizes

| Module | Lines |
|---|---|
| `__init__.py` | 11 |
| `cli.py` | 879 |
| `config.py` | 150 |
| `formatters.py` | 153 |
| `git.py` | 97 |
| `jira_api.py` | 172 |
| `tui/theme.py` | 172 |
| `tui/modals.py` | 143 |
| `tui/ticket_picker.py` | 829 |
| `tui/branch.py` | 274 |
| `tui/file_picker.py` | 418 |
| `tui/pr_picker.py` | 379 |
| `tui/prune.py` | 168 |
| **Total** | **3,845** |

### Notes
- `ticket_picker.py` (829 lines) and `cli.py` (879 lines) exceed the 500-line ideal but are the natural homes for their respective logic. Further splitting would create artificial boundaries.
- `build_ticket_info()` in `tui/theme.py` eliminated duplication between `TicketInfoModal` and `BranchPromptApp`.
- Shared CSS constants in `tui/theme.py` eliminated 5+ copies of DataTable/Footer/context bar CSS.
