Metadata-Version: 2.4
Name: jira-git-helper
Version: 0.23.0
Summary: JIRA ticket context manager for git workflows
Project-URL: Homepage, https://github.com/YOUR_USERNAME/jira-git-helper
Project-URL: Bug Tracker, https://github.com/YOUR_USERNAME/jira-git-helper/issues
Author: Ross Cousens
License: MIT
License-File: LICENSE
Keywords: cli,developer-tools,git,jira,workflow
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Version Control
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Requires-Dist: click>=8.3.1
Requires-Dist: jira>=3.10.5
Requires-Dist: requests>=2.32.0
Requires-Dist: textual>=8.0.0
Description-Content-Type: text/markdown

# jira-git-helper

A terminal-based JIRA ticket context manager for git workflows, invoked as `jg`.

`jg` keeps track of which JIRA ticket you're working on so that branch names,
commit messages, and PR lookups are automatically prefixed — without you having
to type the ticket key every time.

## How it works

`jg` maintains an **active ticket** for each terminal session (e.g. `SWY-1234`).
Once set, commands like `jg commit`, `jg branch`, and `jg push` automatically use
that ticket — you never have to copy-paste it again.

```
$ jg set          # pick a ticket interactively
$ jg branch fix   # creates SWY-1234-fix and switches to it
$ jg add          # stage files and commit — ticket prefix added automatically
$ jg push         # pushes branch and opens the linked PR
```

Each terminal window can track a different ticket independently.

---

## Installation

```sh
uv tool install jira-git-helper
```

Or with pipx:

```sh
pipx install jira-git-helper
```

---

## Quick start

**1. Connect to JIRA**

```sh
jg config set server https://yourcompany.atlassian.net
jg config set email  you@yourcompany.com
jg config set token  <your-jira-api-token>
```

Generate a token at: https://id.atlassian.com/manage-profile/security/api-tokens

**2. Set up the shell hook**

The hook lets each terminal track its own ticket independently. See [Shell hook](#shell-hook) for full details and shell-specific instructions.

**3. (Optional) Show the active ticket in your prompt**

See [Prompt integration](#prompt-integration) for fish/Tide, bash, and zsh instructions.

**4. (Optional) Scope tickets to your projects**

```sh
jg config set projects SWY
# or multiple:
jg config set projects SWY,DOPS
```

**5. Pick a ticket and start working**

```sh
jg set        # opens an interactive picker
jg            # shows the active ticket at any time
```

---

## Configuration

Config is stored in `~/.config/jira-git-helper/config`. Use `jg config set/get/list` to manage it.

### Required

| Key | Description |
|---|---|
| `server` | Your JIRA instance URL, e.g. `https://yourcompany.atlassian.net` |
| `email` | Your JIRA account email |
| `token` | Your JIRA API token |

### Optional

| Key | Description |
|---|---|
| `projects` | Comma-separated project keys to scope the ticket picker, e.g. `SWY` or `SWY,DOPS` |
| `fields.<PROJECT>` | Comma-separated JIRA field IDs to show as extra columns in `jg set` (see below) |
| `fmt_on_add` | Set to `true` to run formatters automatically before the commit prompt in `jg add` |
| `open_on_push` | Set to `true` to open the matching PR (or new-PR URL) in the browser after `jg push` |

### Project scoping

Without `projects` set, `jg set` shows all tickets assigned to you across JIRA.
With `projects` set, results are scoped to just those projects:

```sh
# Single project
jg config set projects SWY

# Multiple projects — results from all projects are merged into one list
jg config set projects SWY,DOPS
```

### Named filters

Each project can have any number of named JQL filters, managed interactively inside
`jg set` by pressing `f`. One filter can be marked as the default — it is loaded
automatically every time `jg set` opens.

**JQL resolution order** (for a given project):
1. Session-active filter (set via Enter in the filter modal — not persisted across runs)
2. Persisted default filter (set via Space in the filter modal — saved to config)
3. Built-in default: `project = PROJECT AND assignee = currentUser() ORDER BY updated DESC`

Filters are visible in `jg config list`.

### Extra columns in `jg set`

Use `fields.<PROJECT>` to add custom JIRA fields as columns in the ticket picker. The easiest
way to discover field IDs is the built-in field picker: open `jg set`, press `d` on any ticket,
then space to toggle fields and Enter to save.

Or set them manually:

```sh
jg config set fields.SWY customfield_10234,customfield_10567
```

Field values are also included in filter-bar searches.

### View your current config

```sh
jg config list
```

This shows all standard keys plus any named filters configured for each project.

---

## Shell hook

The hook does three things:

1. **Seeds `JG_TICKET`** from the last-used ticket when a new shell opens (so you
   don't start from scratch every time).
2. **Keeps terminals isolated** — `jg set` in one terminal updates only that
   terminal's `JG_TICKET`. Other open terminals are unaffected.
3. **Updates `JG_TICKET`** after `jg set`, and clears it after `jg clear`.

Without the hook, all terminals share the same ticket via the state file.

Fish — add to `~/.config/fish/config.fish`:
```fish
eval (jg hook)
```

Bash — add to `~/.bashrc`:
```sh
eval "$(jg hook --shell bash)"
```

Zsh — add to `~/.zshrc`:
```sh
eval "$(jg hook --shell zsh)"
```

### How the hook keeps shells isolated

The hook sets a `JG_TICKET` environment variable that is local to each shell session.
When `JG_TICKET` is defined (even if empty), `jg` commands use it exclusively and
never read the shared state file. This prevents one shell from accidentally seeing
another shell's ticket.

- **`jg set`** → writes the ticket to the state file *and* updates `JG_TICKET` in the
  current shell.
- **`jg clear`** → deletes the state file *and* sets `JG_TICKET` to an empty string.
  The empty string is important — it tells `jg` "there is no ticket" without falling
  back to the state file (which another shell may have since written to).
- **New shell** → the seed block reads the state file once and sets `JG_TICKET`. After
  that, the state file is not consulted again in that shell.

### Updating the hook after upgrading

The hook is generated at eval time. After upgrading `jg`, re-source the hook in any
open shells to pick up changes:

```sh
# fish
source (jg hook | psub)

# bash / zsh
eval "$(jg hook --shell bash)"   # or zsh
```

Or simply restart your terminal.

---

## Prompt integration

### Fish / Tide

Display the active ticket in your [Tide](https://github.com/IlanCosman/tide) prompt.
Run once to install the prompt item:

```sh
jg setup
```

Then follow the printed instructions to add `jg` to your Tide prompt items.

> The `jg setup` command writes `~/.config/fish/functions/_tide_item_jg.fish`, which
> reads the shell-local `$JG_TICKET` variable — so each terminal shows its own ticket.

### Bash

The hook defines a `__jg_ps1` helper. Splice it into your `PS1` in `~/.bashrc`
(after the `eval` line):

```sh
PS1='$(__jg_ps1)\$ '
```

Or anywhere inside an existing prompt string, e.g.:

```sh
PS1='\u@\h $(__jg_ps1)\$ '
```

### Zsh

Same helper, different variable. Add to `~/.zshrc` (after the `eval` line):

```sh
PROMPT='$(__jg_ps1)%% '
```

`__jg_ps1` prints the active ticket followed by a space, or nothing if no ticket is set.

---

## Commands

### `jg`

Show the active ticket for the current session.

```sh
$ jg
SWY-1234
```

---

### `jg set [TICKET]`

Set the active ticket. With no argument, opens an interactive picker that fetches
tickets from JIRA based on your configured projects and JQL.

```sh
jg set             # interactive picker
jg set SWY-1234    # set directly without opening the picker
```

**Flags:**

| Flag | Description |
|---|---|
| `--jql "..."` | Use a raw JQL query instead of configured project JQL. Useful for one-off searches without changing your config. |
| `--max N` | Maximum number of tickets to fetch (default: `200`) |

**Examples:**

```sh
# Show only high-priority tickets, one-off
jg set --jql "project = SWY AND priority = Highest ORDER BY created DESC"

# Fetch more results than the default
jg set --max 500
```

**Interactive picker controls:**

| Key | Action |
|---|---|
| `↑` / `↓` | Move between tickets |
| `/` | Open filter bar — type to narrow by key, summary, assignee, status, or any custom column |
| `Enter` | Select the highlighted ticket (or confirm filter and return to list) |
| `t` | Toggle between flat table view and hierarchical tree view (epics → tasks → subtasks) |
| `i` | Open a detail panel for the highlighted ticket (summary, status, assignee, description, etc.) |
| `o` | Open the highlighted ticket in your browser |
| `c` | Copy the ticket URL to the clipboard |
| `d` | Open the field picker — browse all fields on the ticket, space to toggle columns, Enter to save |
| `f` | Open the filter manager for the current ticket's project |
| `r` | Refresh — re-query JIRA and reload the list |
| `Escape` | Close filter / cancel |

**Tree view (`t`):**

Press `t` to switch to a hierarchical view that groups tickets by epic → task → subtask. Selecting a ticket in tree view sets it as the active ticket, same as the flat table. The filter bar works in tree view too — non-matching branches are hidden, and parent nodes with matching descendants are shown dimmed.

**Filter manager (`f`):**

Pressing `f` opens a per-project filter list. The active filter is shown in a status
bar above the footer — `*` means no custom filter (built-in default JQL), otherwise
the filter name is shown, e.g. `SWY: Sprint  DOPS: *`.

| Marker | Meaning |
|---|---|
| `●` | Persisted default, currently active |
| `▶` | Session-activated (Enter) — active this run only, not persisted |
| `○` | Persisted default, but currently overridden by a session-activated filter |

| Key | Action |
|---|---|
| `Enter` | Activate filter for this session (not saved as default) |
| `Space` | Set filter as the persisted default (saved to config) |
| `n` | Create a new filter — prompts for a name, then a JQL query |
| `e` | Edit the JQL of the selected filter |
| `d` | Delete the selected filter (with confirmation) |
| `Escape` | Close the filter manager |

---

### `jg clear`

Clear the active ticket for the current session.

```sh
jg clear
```

---

### `jg info [TICKET]`

Show a rich summary panel for a ticket, including: summary, status, priority,
assignee, reporter, labels, URL, and a description excerpt (truncated at 800 chars).

```sh
jg info            # uses the active ticket
jg info SWY-5678   # look up any ticket by key
```

---

### `jg debug <ticket>`

Dump every raw JIRA field for a ticket, formatted as syntax-highlighted JSON. Only non-null fields are shown.

```sh
jg debug SWY-1234
```

Useful for discovering custom field IDs, inspecting the API shape of a specific ticket type, or diagnosing issues with the tree view hierarchy.

---

### `jg open [TICKET]`

Open a ticket in your browser.

```sh
jg open            # opens the active ticket
jg open SWY-5678   # open any ticket by key
```

---

### `jg branch [name]`

Work with git branches scoped to the active ticket.

**With no arguments** — fetches from origin, then shows an interactive picker with all local and remote branches matching the active ticket. Each branch shows its tracking status (`tracked`, `local`, or `remote`). If no matching branches exist, goes straight to the new branch prompt.

```sh
jg branch
```

**With a name** — creates a new branch named `TICKET-branch-name` (using `git switch -C`)
and switches to it.

```sh
jg branch my-feature    # creates SWY-1234-my-feature
```

**Interactive picker controls:**

| Key | Action |
|---|---|
| `↑` / `↓` | Move between branches |
| `/` | Open filter bar — type to narrow by branch name or tracking status |
| `Enter` | Switch to the highlighted branch (or confirm filter and return to list) |
| `n` | Create a new branch — opens the branch prompt with ticket info and suffix input |
| `Escape` | Close filter / cancel |

**Tracking column:**

| Value | Colour | Meaning |
|---|---|---|
| `tracked` | green | Local branch with remote upstream |
| `local` | amber | Local-only branch |
| `remote` | cyan | Remote-only branch (no local checkout) |

**Status column:**

| Value | Colour | Meaning |
|---|---|---|
| `never pushed` | amber | Local branch that was never pushed |
| `remote deleted` | red | Remote upstream was deleted (e.g. after PR merge) |
| `remote only` | cyan | Exists on remote but no local checkout |
| *(empty)* | | Healthy tracked branch |

---

### `jg add`

An interactive TUI for staging files and committing — all in one step.

```sh
jg add
```

If run on `main`/`master`, a branch prompt screen appears first — it shows the active ticket's info and a suffix input. After the branch is created you are switched to it and the staging screen opens.

The screen is split into sections: **Staged** (always shown), plus **Modified**, **Deleted**, and **Untracked** sections that appear only when they have files. Use `Space` to toggle files between staged/unstaged, then `Enter` to open the commit message prompt. The commit message is automatically prefixed with the active ticket key.

**File picker controls:**

| Key | Action |
|---|---|
| `↑` / `↓` | Move between files |
| `Space` | Stage or unstage the highlighted file |
| `f` | Run formatters — opens a results modal, then reloads git status on close |
| `/` | Open filter bar for the focused section |
| `Enter` | Open commit message prompt (or confirm filter and return to list) |
| `Escape` | Close filter / cancel |

**Commit message prompt:**

Type a message and press `Enter` to commit. Press `Escape` to skip the commit and just apply the staged changes — useful when you want to stage files now and commit later with `jg commit`.

If `fmt_on_add` is set to `true` in config, formatters run automatically before the commit prompt opens. If a formatter modifies a staged file (dropping it back to modified), the commit is paused and a warning is shown so you can review and re-stage before trying again:

```sh
jg config set fmt_on_add true
```

> **Note:** If no ticket is set, `jg add` will prompt you to pick one interactively before proceeding.
>
> **Note:** If the current branch is not prefixed with the active ticket key, `jg add` will prompt for a branch suffix and create the branch automatically before committing.

---

### `jg commit <message>`

Commit with the active ticket key automatically prepended to the message.

```sh
jg commit "fix login redirect"
# runs: git commit -m "SWY-1234 fix login redirect"
```

Any extra arguments after the message are passed through to `git commit`:

```sh
jg commit "fix login redirect" --no-verify
jg commit "fix login redirect" --amend
```

> **Note:** Refuses to run on `main` or `master`. Use `jg branch <name>` to create a feature branch first.

---

### `jg fmt`

Run formatters against all modified, staged, and untracked files in the current repo.

```sh
jg fmt
```

Results are shown as a table with columns for status, file, formatter name, exit code, and a note:

```
  File              Formatter             Exit  Note
✓ main.hcl          eof                   0
✓ main.hcl          terragrunt-hcl-fmt    0
✗ broken.hcl        terragrunt-hcl-fmt    1     invalid HCL on line 4
— logo.png          —                     —     skipped (binary)
```

**Built-in formatter: `eof`**

The `eof` formatter always runs on every text file. It ensures each file ends with exactly one newline, stripping any extra trailing newlines. It cannot be removed.

**Binary file detection**

Before running any formatter, `jg fmt` uses `git ls-files --eol` to identify binary files. Binary files are shown in the table as `skipped (binary)` and no formatter is run on them, even if their extension would otherwise match.

**User-configured formatters**

Each formatter has a name, a file glob (matched against the filename), and a command. Use `{}` in the command as a placeholder for the absolute file path — similar to `find -exec`.

```sh
jg fmt add                        # interactive prompts for name, glob, and command
jg fmt add terragrunt-hcl-fmt     # supply the name upfront
jg fmt edit terragrunt-hcl-fmt    # edit glob and command (current values pre-filled)
jg fmt list                       # list all configured formatters
jg fmt delete terragrunt-hcl-fmt  # remove a formatter by name
```

**Example — adding a HCL formatter:**

```
Formatter name: terragrunt-hcl-fmt
File glob (e.g. *.hcl, *.tf): *.hcl
Command (use {} for the filename): /usr/local/bin/terragrunt hcl fmt {}
```

**Formatting only files changed in the current branch:**

```sh
jg fmt diff
```

Runs all formatters over files changed between the current branch and the default branch. Useful before opening a PR to ensure only your changes are formatted, without touching unrelated files. Exits with a warning if run on `main`/`master` or a detached HEAD.

Formatter configuration is stored in `~/.config/jira-git-helper/config` under the `fmt` key and is also visible in `jg config list`.

---

### `jg push`

Push the current branch to origin (`git push -u origin HEAD`).

```sh
jg push
```

When `open_on_push` is enabled, `jg push` looks up open PRs linked to the active
ticket in JIRA and opens the one matching the current branch. If no matching PR
exists but GitHub printed a "Create a pull request" URL during the push, it opens
that instead.

```sh
jg config set open_on_push true
```

---

### `jg reset`

Switch to the default branch and pull the latest from origin.

```sh
jg reset
```

The default branch is detected from `origin/HEAD`, falling back to `main` or `master`.

If uncommitted changes would block the branch switch, `jg reset` offers to stash them
and continue. After a successful pull it then offers to restore the stash.

---

### `jg sync`

Rebase the current feature branch onto the latest default branch from origin, without leaving your branch.

```sh
jg sync
```

Equivalent to `git fetch origin && git rebase origin/main`. The default branch is auto-detected from `origin/HEAD`, falling back to `main`/`master`.

If a conflict occurs, the standard git rebase flow applies — resolve conflicts and run `git rebase --continue`, or `git rebase --abort` to cancel.

> Use `jg reset` when you want to switch *to* the default branch and pull. Use `jg sync` when you want to stay on your feature branch and bring it up to date.

---

### `jg prune`

Interactively prune local branches that have no remote counterpart — either because the remote was deleted (typically after a PR merge) or because the branch was never pushed.

```sh
jg prune
```

Runs `git fetch --prune` first to refresh remote-tracking refs, then opens an interactive DataTable listing every prunable local branch. The current branch and default branch are always excluded.

**Status column:**

| Status | Colour | Meaning |
|---|---|---|
| `remote deleted` | amber | Had a remote tracking branch that was deleted |
| `never pushed` | cyan | Local-only branch that was never pushed |

**Controls:**

| Key | Action |
|---|---|
| `↑` / `↓` | Navigate branches |
| `Space` | Toggle selection |
| `a` | Select / deselect all |
| `d` | View diff against default branch in a full-screen viewer |
| `x` | Delete selected branches (confirmation prompt) |
| `s` | Switch to the highlighted branch and exit |
| `Escape` | Quit without changes |

Uses `git branch -D` (force delete) so branches merged via squash or rebase are handled correctly alongside standard merges.

---

### `jg prs [TICKET]`

Browse all GitHub PRs linked to a ticket in an interactive TUI.

```sh
jg prs             # uses the active ticket
jg prs SWY-5678    # browse PRs for any ticket
```

PRs are fetched from JIRA and supplemented with GitHub CLI results (if `gh` is installed and authenticated). Columns shown: Source, Status, Author, Repo, Source branch, Title, Updated. PRs are sorted with GitHub PRs first, then open before merged/declined, then by last-updated date. Status is colour-coded: green (open), yellow (draft), blue (merged), red (declined).

**Controls:**

| Key | Action |
|---|---|
| `↑` / `↓` | Move between PRs |
| `/` | Open filter bar — searches status, author, repo, branch, and title |
| `o` | Open the highlighted PR in your browser |
| `d` | View the PR diff inline (uses [`delta`](https://github.com/dandavison/delta) if installed) |
| `s` | Switch to the PR's source branch (checks out the remote tracking branch if not found locally) |
| `Escape` | Close filter / quit |

> **Requires:** [`gh` CLI](https://cli.github.com) installed and authenticated.

---

### `jg config get <key>`

Print a single config value.

```sh
jg config get server
jg config get jql.SWY
```

Exits with a non-zero status if the key is not set.

---

### `jg config set <key> <value>`

Set a config value. Standard keys are `server`, `email`, `token`, and `projects`.

```sh
jg config set server   https://yourcompany.atlassian.net
jg config set email    you@yourcompany.com
jg config set token    <api-token>
jg config set projects SWY,DOPS
```

Named JQL filters are managed interactively with the `f` key inside `jg set`.

---

### `jg config list`

List all configured values. Masks the `token` value for safety. Automatically
shows any `jql.<PROJECT>` keys you have set.

```sh
jg config list
```

---

### `jg hook [--shell fish|bash|zsh]`

Print the shell hook function to stdout. Intended to be evaluated in your shell
startup file (see [Shell hook](#shell-hook) above).

```sh
jg hook                  # fish (default)
jg hook --shell bash
jg hook --shell zsh
```

| Flag | Description |
|---|---|
| `--shell fish\|bash\|zsh` | Shell to emit the hook for (default: `fish`) |

The bash/zsh hook also defines `__jg_ps1` for prompt integration (see
[Prompt integration](#prompt-integration)).

---

### `jg setup`

Configure fish/Tide prompt integration. Creates
`~/.config/fish/functions/_tide_item_jg.fish` and prints the follow-up `set -U`
commands needed to activate and style the prompt item.

```sh
jg setup
```

> Fish/Tide only. For bash/zsh prompt integration, see [Prompt integration](#prompt-integration).

---

### `jg version`

Print the installed version.

```sh
jg version
# or
jg --version
```

---

## Requirements

- Python 3.10+
- `gh` CLI — required for PR diff viewing in `jg prs` — https://cli.github.com

---

## License

MIT
