Corporate git workflow assistant — ticket-prefix discipline and branch protection in one Rust binary.
tix enforces a few small things every team needs and nothing else:
- Every commit is prefixed with a ticket ID (
POD-1234 fix login). - Direct commits and pushes to
main,master,develop,release/*are blocked. - New branches are created off the latest base with a consistent name (
feature/POD-1234-fix-login). - Pushing a stale branch warns; opening a PR shells out to
gh/glabor prints the URL.
It's offline, has no telemetry, and uninstalls cleanly.
cargo install --git https://github.com/ObieMunoz/tix
tix inittix init installs three hook shims to ~/.config/tix/hooks/ and points git's global core.hooksPath at them. It refuses to overwrite an existing core.hooksPath without --force. Use --dry-run to preview the actions.
# 1. Install
cargo install --git https://github.com/ObieMunoz/tix
tix init
# 2. Start a branch off the latest main
tix start POD-1234 fix-login
# → feature/POD-1234-fix-login
# 3. Edit, commit — message gets prefixed automatically
git commit -m "drop legacy session token"
# → "POD-1234 drop legacy session token"
# 4. Push and open a PR
git push -u origin HEAD
tix pr # → opens gh/glab or prints the compare URLIf you already have unprefixed commits on a branch:
tix set-ticket POD-1234
# previews the rewrite, prompts, then rebases. Refuses if any commit
# is already on a remote ref unless --force.For a branch that shouldn't have a ticket:
tix clear-ticket| Feature | How |
|---|---|
| Ticket prefix on commits | prepare-commit-msg rewrites the first real line of the message to <TICKET> <subject>. Idempotent. Skips merge/squash. Leaves a different ticket alone. |
| First-time prompt | pre-commit prompts on a branch with no state entry. Empty input → no-ticket mode. Non-TTY refuses with a clear hint to run tix set-ticket / tix clear-ticket. |
| Protected branches | pre-commit and pre-push block direct work on main, master, develop, release/*. Glob patterns are single-segment (no **). Branch deletion is allowed. --no-verify bypasses (see Limitations). |
| Retroactive amend | tix set-ticket POD-1234 rewrites the subject of every unprefixed unpushed commit reachable from HEAD via cherry-pick. Body is preserved. Refuses to rewrite remote-side commits unless --force. |
tix start |
Validates ticket, refuses on a dirty tree, fetches origin/<base>, creates <start_prefix>/<TICKET>[-<slug>], registers the ticket on the new branch. |
| Branch-naming check | Per branches.naming_enforcement: warn (default) prints to stderr, block refuses, off skips. |
| Stale-base warning | pre-push warns when the pushed branch is more than push.stale_warn_threshold (default 50) commits behind origin/<default_base>. Best-effort; no network → no warning. Never blocks. |
tix pr |
Detects provider from origin (github / gitlab / bitbucket), shells to gh/glab if installed and pr_command = auto, else prints the compare/MR URL. |
tix ticket [open] |
Substitutes {ticket} in integrations.ticket_url_template and prints (or opens) the URL. |
| Command | Purpose |
|---|---|
tix init [--dry-run] [--force] |
Install global hooks, scaffold ~/.config/tix/config.toml. |
tix uninstall [--dry-run] [--purge] |
Remove the managed shims, unset core.hooksPath (only if it points at our dir). --purge also drops the config file. |
tix start <TICKET> [DESCRIPTION] [--base <BRANCH>] |
Create a feature branch off the latest base. |
tix set-ticket <TICKET> [--force] [--yes] |
Set/replace ticket on the current branch; offers retroactive amend. |
tix clear-ticket |
Put the current branch in no-ticket mode. |
tix show |
Print branch, ticket, protected status, base, config sources. |
tix protect <BRANCH> [--global | --repo] |
Add a pattern to the protected list (defaults preserved). |
tix unprotect <BRANCH> [--global | --repo] |
Remove a pattern. |
tix config get | set | list |
Read / write / list config values. |
tix doctor [--verbose] |
Diagnostic checks across the install. |
tix pr |
Open a PR for the current branch. |
tix ticket [open] |
Print or open the current branch's ticket URL. |
Three layers, last-wins:
- Built-in defaults (compiled in).
- Global config:
~/.config/tix/config.toml(or$XDG_CONFIG_HOME/tix/config.toml). - Repo config:
<repo>/.tix.toml— committable, encodes team rules.
Per-clone state (never committed) lives at <repo>/.git/tix/state.json.
| Key | Type | Default | Effect |
|---|---|---|---|
ticket.pattern |
regex | ^[A-Z]+-\d+$ |
What counts as a ticket. |
ticket.prefix_format |
string | {ticket} {message} |
Format used when prefixing. |
branches.protected |
list | ["main", "master", "develop", "release/*"] |
Glob patterns blocked from direct commits/pushes. |
branches.default_base |
string | main |
Base branch for tix start and the stale-base check. |
branches.start_prefix |
string | feature |
Prefix used in branch names by tix start. |
branches.naming_pattern |
regex | ^(feature|bugfix|hotfix|chore)/[A-Z]+-\d+(-.+)?$ |
Naming check pattern. |
branches.naming_enforcement |
enum | warn |
warn / block / off. |
push.stale_warn_threshold |
u32 | 50 |
Commits behind base before warning; 0 disables. |
integrations.ticket_url_template |
string | "" |
URL template with {ticket} (e.g. https://company.atlassian.net/browse/{ticket}). |
integrations.pr_provider |
string | github |
Fallback when origin host doesn't match a known provider. |
integrations.pr_command |
string | auto |
auto shells to gh/glab if present; anything else prints the URL. |
tix config get branches.default_base
tix config set branches.default_base develop
tix config set integrations.ticket_url_template "https://example.atlassian.net/browse/{ticket}"
tix config set branches.protected --append trunk # add to the list (idempotent)
tix config set branches.protected --remove trunk # remove from the list
tix config set branches.start_prefix bug --repo # team rule, committed in .tix.toml
tix config list # resolved view, with sources
tix config list --global # only what's in ~/.config/tix/config.tomltix uninstall # removes shims + unsets core.hooksPath; keeps config.toml
tix uninstall --purge # also removes ~/.config/tix/tix uninstall only unsets core.hooksPath if it points at our managed dir — if you've redirected hooks elsewhere, that setting is left alone. Per-clone state in <repo>/.git/tix/state.json is left untouched (the tool can't enumerate clones).
If tix is uninstalled but the shims linger, they're a silent no-op — every shim short-circuits when tix isn't on PATH.
- Client-side enforcement. Hooks bypass with
--no-verify. Pair with server-side branch protection (GitHub / GitLab settings) for hard guarantees. - Single-segment globs.
release/*matchesrelease/1.0but notrelease/1.0/rc1. No**. - Unix-only. macOS and Linux. The hook shims are POSIX shell.
- Offline.
tix doctorchecks origin, but no command makes network requests other thangit fetch.
cargo build # debug build
cargo test # unit + integration tests (uses real git in temp dirs)
cargo clippy --all-targets -- -D warnings
cargo fmtTests are end-to-end where possible: each integration test spins up a temp $HOME, $XDG_CONFIG_HOME, GIT_CONFIG_GLOBAL, and a fresh git repo (sometimes with a bare-repo origin). Hook tests exercise the shim → tix → hook → message-rewrite chain via real git commit / git push with the cargo-built binary on PATH.
The repo ships its own .tix.toml that turns off branch protection and naming enforcement for this repo only:
[branches]
protected = []
naming_enforcement = "off"Without it, the maintainer (and any contributor with tix installed) couldn't commit to main or push release tags without --no-verify — the very tool would fight its own dev workflow. Global defaults still apply to every other repo on your machine.
On a fresh clone, the per-clone state in .git/tix/state.json doesn't exist yet, so the first git commit on main will prompt for a ticket. Run this once and the prompt won't return:
tix clear-ticketThis puts main into "no-ticket mode" for your clone — the entry persists, so subsequent commits skip the prompt. (Per-clone state is intentionally not committed; each contributor decides their own ticket-vs-no-ticket choice.)
Two deliberate deviations from SPEC.md, documented in commits:
- Shim form. The literal SPEC shim (
exec command -v tix > /dev/null && exec tix hook ...) exits 1 whentixis absent — it would block every commit, not silent-no-op. We usecommand -v tix > /dev/null 2>&1 || exit 0; exec tix hook ...instead, which exits 0 when missing. - Config dir. SPEC says
~/.config/tix/config.toml.dirs::config_dir()returns~/Library/Application Supporton macOS; we honor the SPEC's stated path on every platform via$XDG_CONFIG_HOME(or~/.config/tix).
MIT. See LICENSE.