# π GitLab MR Tracker
[](https://github.com/julien-langlois/gitlab-tracker/actions)
[](https://crates.io/crates/gitlab-tracker)
[](https://crates.io/crates/gitlab-tracker)
[](LICENSE)
[](https://www.rust-lang.org/)
**GitLab MR Tracker** is a fast, asynchronous Terminal User Interface (TUI) dashboard designed for engineering teams. It provides real-time verification of GitLab Merge Requests across target environment branches (`main`, `preproduction`, `staging`, etc.), handling strict SHA verification as well as cherry-picked commit identification.

## β¨ Key Features
* π **OS Keyring Integration (Zero Plain-Text Secrets):** Personal Access Tokens (PAT) can be securely stored directly in your OS secret manager (GNOME Keyring, KWallet, macOS Keychain, or Windows Credential Manager).
* π·οΈ **Dynamic Scoped Labels & Custom Chips:**
* **Smart Filtering:** Configure specific label prefixes (e.g., `deploy::`, `review::`) to display cleanly as colored chips in the main table grid, while keeping **all** attached tags visible in the side inspector panel.
* **Customizable Palette:** Map label names or wildcard patterns (e.g., `deploy::*`) to custom terminal colors or standard HEX codes (`#FF5733`) via an XDG-compliant JSON config. Labels without a config override automatically fall back to their **GitLab-side colour** (fetched at startup), with foreground computed for legibility.
* β‘ **High Performance & Asynchronous:** Powered by `tokio` and `reqwest`, utilizing non-blocking event loops and bounded concurrent requests via semaphores to protect GitLab API rate limits.
* π‘οΈ **Pass-Through Pass Caching:** Core MR metadata (author, milestone, assignee, description, labels) is fetched once and cached locally. Fully deployed MRs bypass network re-queries entirely ("Green Pass").
* π **Dual Match Verification Engine:**
* **System 1 (Strict SHA):** Validates precise merge/squash commit SHAs on target branches (resistant to `git reset --hard`).
* **System 2 (Intelligent Fuzzy Matcher):** Uses a keyword relevance matrix to verify cherry-picked commits deployed across branches.
* π₯οΈ **Responsive Flexbox TUI Grid:** Features a dynamic layout engine (`Constraint::Fill`) that seamlessly scales table columns and side panels from 1080p laptop displays to ultra-wide 4K monitors without empty trailing spaces.
* π **Smart Auto-Sorting by Last Update:** The dashboard defaults to sorting MRs by `updated_at` (most recently pushed to remote first), automatically re-applied after each refresh. Cycle through sort columns (`S`) and toggle direction (`Shift+S`). The active sort is always visible in the table title bar.
* π **Browser Integration:** Open any selected MR directly in your default browser with a single keypress (`O`).
* π **Smart Desktop Notifications:** Receives native OS desktop notifications **only when an MR's branch status has changed** since the last run β no duplicate alerts on restart or redundant refreshes.
* β¨ **Refresh Highlight:** After each background refresh, any MR whose `updated_at` timestamp has changed since the previous cycle is briefly highlighted in the table with a green tint. The highlight fades out automatically after ~10 seconds.
* π **XDG-Compliant Persistence:** Saves tracked dashboard state, UI configurations, and last-known branch statuses automatically to platform-standard configuration paths using `directories`.
* **Customizable Refresh Interval:** Tailor the background polling rate to your needs (defaults to 15 minutes / 900s) via `config.json` or the `GITLAB_REFRESH_INTERVAL_SECS` environment variable.
* π **Activity Badge:** Each MR in the Context Inspector displays a color-coded activity badge based on its `updated_at` timestamp β π’ Active, π‘ Slowing, or π΄ Stale. Thresholds are fully configurable via `config.json` or environment variables (`ACTIVITY_RECENT_DAYS`, `ACTIVITY_STALE_DAYS`).
* π¬ **Notes Indicator:** The total number of comments and discussion threads (`user_notes_count`) is fetched from the GitLab API at no extra cost and displayed both in the optional **Notes** table column and in the Context Inspector. A yellow `π¬ N` badge signals that comments are awaiting attention; a dimmed `β No comments` confirms there is nothing to address.
* π **Animated Mergeability Badge:** For open MRs, the Status column alternates every second between the base `Open` badge and a live mergeability indicator sourced directly from the GitLab API:
| Badge | Color | Meaning |
| :--- | :--- | :--- |
| `Mergeable` | π© Light green | MR can be merged cleanly |
| `Conflict` | π₯ Red | Merge conflicts must be resolved |
| `Rebase` | π¨ Yellow | Branch is behind target β rebase required |
No extra column is added: the animation keeps the layout compact while surfacing critical merge-readiness at a glance.
* ποΈ **Toggleable Table Columns (`C`):** Press `C` at any time to open an interactive column picker popup. Use `β`/`β` to navigate and `Space` to toggle each optional column on or off. Your selection is **instantly saved** to `config.json` and persisted across restarts β no manual file editing required. Available optional columns:
| Column | Description |
| :--- | :--- |
| **Activity** | Color-coded activity badge β π’ Active, π‘ Slowing, π΄ Stale (same thresholds as the Inspector) |
| **Target** | The branch the MR is intended to merge into |
| **Labels** | Filtered label chips (respects `table_label_prefixes`) |
| **Milestone** | The associated milestone title |
| **Notes** | Total number of comments and discussion threads β `π¬ N` in yellow when non-zero, dimmed `β 0` otherwise |
All columns are hidden by default to keep the layout compact. They can also be enabled statically via `visible_columns` in `config.json` (see configuration section below).
* β **MR Flagging & Advanced Filters:** Manually flag any MR with `Space` to mark it with a coloured star chevron (β
) in the title column. Press `F` to open the **filter picker popup**, which lets you narrow the table by:
* `Flagged β
` β only your manually flagged MRs
* **GitLab state** β `Opened`, `Merged`, or `Closed`
* **Mergeability** β `Mergeable`, `Conflict`, `Needs Rebase`, `Not Approved`, `Requested Changes`, `Draft`, `Discussions`
* **Has comments** β MRs with at least one note or discussion thread
* **Milestone** β free-text search on the milestone title (case-insensitive)
* **Assignee** β free-text search on the GitLab assignee and, when a tracker ticket is linked (e.g. Redmine), its assignee as well
The active filter is shown in the table header. Flagged state is **persisted across restarts** via `tracker_state.json`.
* π **Milestone Bulk-Add (Release Manager Workflow):** In Insert mode, type `@` followed by any part of a milestone name to trigger a live autocomplete dropdown. Active and upcoming milestones are fetched from GitLab on startup and filtered in real time as you type. Selecting a milestone with `Enter` automatically adds **all open MRs attached to that milestone** in a single action β no need to enter IDs one by one. Ideal for release managers preparing a deployment checklist.
```text
i β Enter Insert mode
@5.2 β filters milestones containing "5.2"
β / Tab β navigate suggestions
Enter β bulk-add all open MRs from the selected milestone
Esc β close dropdown without selecting
```
* π¬ **Pipeline Inspector (`P`):** Press `P` on any selected MR to toggle the side panel between MR metadata and its pipeline history. The last 5 pipeline runs are displayed with per-stage job breakdown, status icons, and execution durations:
```text
#9981 β passed
βΈ test
β lint (18s)
β unit-tests (74s)
βΈ build
β build (42s)
βΈ deploy
β deploy-staging (31s)
```
Pipeline data is fetched **alongside MR metadata** in the same refresh cycle and **persisted to disk** β so it is immediately available on restart without an extra network call. Re-fetching only occurs when GitLab reports a new `updated_at` timestamp, keeping API usage minimal.
---
## π Authentication & Configuration
The application requires your GitLab Project configuration and an API Personal Access Token.
### Step 1: Set up environment variables (optional)
> **β¨ Zero-config first run:** If no `.env` file or `config.json` is present, `gitlab-tracker` will interactively prompt you for the required values on first launch and persist them automatically to `config.json`. No manual file setup is needed.
```text
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FIRST-RUN INTERACTIVE ONBOARDING β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β π GitLab URL [https://gitlab.com]: _ β
β π’ GitLab Project ID: _ β
β π GitLab Personal Access Token: _ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
For teams and CI pipelines, you can still pre-configure everything via a `.env` file to skip the prompts entirely:
1. Copy the provided template to create your local `.env` file:
```bash
cp .env.example .env
```
2. Open `.env` and specify your project details:
```env
# Required: Your target GitLab Project ID
GITLAB_PROJECT_ID=12345678
# Optional: Custom self-hosted GitLab instance (Defaults to https://gitlab.com if omitted)
GITLAB_URL=https://gitlab.my-company.com
# Optional: Override token via environment variable (Not recommended for disk storage)
# GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
# Optional: Override initial tracked branches for new sessions (comma-separated)
DEFAULT_BRANCHES="main,develop"
# Optional: Filter table column tags by prefix (comma-separated)
TABLE_LABEL_PREFIXES="deploy::,review::"
# Optional: Activity badge thresholds in the Context Inspector (in days)
ACTIVITY_RECENT_DAYS=2 # π’ Green if updated within N days (default: 2)
ACTIVITY_STALE_DAYS=7 # π΄ Red if not updated for N days (default: 7)
```
---
### π Settings Resolution Order
Settings are resolved in the following order (highest to lowest priority):
1. **System Environment Variables & Local `.env`** (current directory)
2. **Global `.env`** (`~/.config/gitlab-tracker/.env`)
3. **User Config File** (`~/.config/gitlab-tracker/config.json`)
4. **Built-in Fallback Defaults** (`https://gitlab.com`, `["main"]` for default branch)
---
### Step 2: First-Run Interactive Onboarding & Keyring PAT Security Layer
Your GitLab personal access token is **never stored in plain text**.
On first launch, `gitlab-tracker` resolves each required value using the following priority order β prompting interactively only as a last resort:
```text
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SETTINGS LOOKUP ORDER β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β GITLAB_PROJECT_ID & GITLAB_URL β
β 1. Environment variable / .env file β
β 2. ~/.config/gitlab-tracker/config.json β
β 3. Interactive CLI prompt β saved to config.json β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β GITLAB_TOKEN β
β 1. GITLAB_TOKEN environment variable (if set) β
β 2. Native OS Keyring (GNOME Keyring / macOS Keychain) β
β 3. Interactive CLI prompt β saved to OS Keyring β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
1. **First-Run Onboarding:**
If no `GITLAB_TOKEN` is found in your `.env` or environment, the application will prompt you interactively in the terminal on its initial launch:
```text
π No GitLab URL found in config or environment.
Leave empty to use the default (https://gitlab.com)
GitLab URL [https://gitlab.com]: https://gitlab.my-company.com
π’ No GitLab Project ID found in config or environment.
Please enter your GitLab Project ID: 12345678
β
Config saved to config.json!
π No GITLAB_TOKEN found in environment or system Keyring.
Please enter your GitLab Personal Access Token: glpat-xxxxxxxxxxxx
β
Token securely saved to OS Keyring!
```
2. **Secure Token Persistence:**
The token is encrypted and handed off directly to your operating system's native secret manager:
* **Linux:** GNOME Keyring / KWallet via Secret Service API
* **macOS:** Apple Keychain Service
* **Windows:** Windows Credential Manager
3. **Subsequent Launches:**
You can delete the `GITLAB_TOKEN` entry from your `.env` completely. On subsequent runs, `gitlab-tracker` retrieves the token silently from the OS Keyring without requiring plain-text files or manual re-entry.
---
### Step 3: UI, Default Branches & Label Customization (`config.json`)
On its first launch, the tool automatically generates a `config.json` file inside your OS user configuration directory:
* **Linux:** `~/.config/gitlab-tracker/config.json`
* **macOS:** `~/Library/Application Support/gitlab-tracker/config.json`
* **Windows:** `C:\Users\<User>\AppData\Roaming\gitlab-tracker\config.json`
You can edit this file to adjust default environment branches, label badge colors, wildcard rules, and activity badge thresholds:
```json
{
"project_id": "12345678",
"gitlab_url": "https://gitlab.my-company.com",
"refresh_interval_secs": 900,
"default_branches": [
"main"
],
"table_label_prefixes": [
"deploy::",
"review::"
],
"activity_recent_days": 2,
"activity_stale_days": 7,
"visible_columns": {
"target_branch": false,
"labels": false,
"milestone": false
},
"label_colors": {
"deploy::*": {
"bg": "#2E7D32",
"fg": "white"
},
"review::approved": {
"bg": "magenta",
"fg": "white"
},
"review::*": {
"bg": "cyan",
"fg": "black"
},
"size::*": {
"bg": "dark_gray",
"fg": "white"
},
"bug": {
"bg": "#D32F2F",
"fg": "white"
}
}
}
```
> **Optional table columns** β By default the table only shows the fixed columns (ID, Title, Status) plus your tracked branches, keeping the layout compact. Enable any optional column individually in `config.json` under `visible_columns`:
>
> | Key | Default | Column shown |
> | :--- | :--- | :--- |
> | `activity` | `false` | **Activity** β π’ Active / π‘ Slowing / π΄ Stale badge |
> | `target_branch` | `false` | **Target** β the branch the MR merges into |
> | `labels` | `false` | **Labels** β filtered label chips (respects `table_label_prefixes`) |
> | `milestone` | `false` | **Milestone** β the associated milestone title |
> | `notes` | `false` | **Notes** β total comment count (`π¬ N` in yellow when non-zero) |
>
> Example β enable Activity and Target only:
>
> ```json
> "visible_columns": {
> "activity": true,
> "target_branch": true,
> "labels": false,
> "milestone": false,
> "notes": false
> }
> ```
> **Activity badge thresholds** control the colored indicator displayed next to the `Updated` field in the Context Inspector:
>
> | Badge | Meaning | Condition |
> | :--- | :--- | :--- |
> | π’ Active | Updated recently | `elapsed days < activity_recent_days` |
> | π‘ Slowing | Activity slowing down | between the two thresholds |
> | π΄ Stale | No recent activity | `elapsed days β₯ activity_stale_days` |
> | β¬ Unknown | Timestamp unavailable | β |
#### π How Desktop Notifications Work:
Notifications are powered by `notify-rust` and rely on your system's native notification daemon (e.g., `libnotify` on Linux, `NSUserNotifications` on macOS).
Four events trigger a desktop notification:
| πΏ **New branch detected** | An MR's commit SHA was found on a branch not present in the last persisted state |
| π **MR updated** | `updated_at` from GitLab differs from the previously stored value |
| π **Mergeability changed** | The mergeability status transitioned (e.g. `Mergeable β Conflict`) |
| π **Milestone changed** | The milestone attached to the MR changed (e.g. `v2.4.0 β v2.5.0`) |
**Anti-spam on startup:** change notifications (`updated_at`, mergeability, milestone) are **suppressed during the initial sync** β the first fetch cycle after launch. This prevents a flood of toasts when the app starts and reconciles its in-memory state with the GitLab API. Only genuine changes detected during subsequent background refreshes (or a manual `R` refresh) will produce notifications.
* β
**No duplicate alerts** when restarting the app with an unchanged state.
* β
**No spam** during the initial sync or redundant refresh cycles.
* β
**Reliable detection** of real changes across refreshes and restarts.
The last-known branch state per MR is persisted in `tracker_state.json` under the `last_known_branches` key.
---
#### πΏ How Branch Resolution Works:
1. **Active Session Priority:** If `tracker_state.json` exists from a previous run, the app restores your last active layout (columns added/removed via input).
2. **First Run / Fresh Session:** If no state exists, initial branches are loaded from `DEFAULT_BRANCHES` in `.env` if provided, falling back to `default_branches` in `config.json` (defaults to `["main"]`).
---
## π¦ Installation
### Recommended β Install from crates.io
The simplest way to install `gitlab-tracker` if you have Rust (1.80+) available:
```bash
cargo install gitlab-tracker
```
This downloads, compiles, and installs the latest published release directly from [crates.io](https://crates.io/crates/gitlab-tracker) into `~/.cargo/bin/`. No cloning required.
### Pre-built Binaries
If you prefer not to compile, download the latest pre-compiled binary for your architecture from the [Releases Page](https://github.com/julien-langlois/gitlab-tracker/releases) and place it somewhere on your `$PATH`.
### Building from Source
For development or to test unreleased changes, clone the repository and build manually:
```bash
git clone git@github.com:julien-langlois/gitlab-tracker.git
cd gitlab-tracker
# Build optimized release executable (builds all workspace members)
cargo build --release
# Optional: install binary globally to ~/.cargo/bin/
cargo install --path gitlab-tracker
# Build without desktop notifications (headless / CI environments)
cargo install --path gitlab-tracker --no-default-features
```
Once installed via any of the methods above, launch the dashboard from any terminal folder:
```bash
gitlab-tracker
```
---
## β¨οΈ Dashboard Navigation & Shortcuts
The dashboard operates in two keyboard modes, inspired by vim:
### π¦ Normal Mode (default)
Shortcut keys are active. The input field is passive.
| `i` or `/` | **Enter Insert mode** β focus the input field |
| `β²` / `βΌ` or `k` / `j` | Navigate rows in the table |
| `Tab` | Cycle focus between panes: **Dashboard β Inspector β Tracker** β Dashboard (Tracker pane only when a ticket is linked) |
| `T` | When focus is on Dashboard or Inspector: **jump to Tracker pane**. When already on Tracker: **open ticket URL** in browser |
| `P` | **Inspector pane focused**: cycle MR Info β Pipelines. **Tracker pane focused**: cycle Ticket Info β Time Log |
| `L` | **Log time** on the linked tracker ticket *(only when a tracker plugin is configured)* |
| `C` | **Open column picker** β toggle optional columns on/off |
| `O` | Open selected MR in your default web browser |
| `R` | Force immediate network refresh for all MRs |
| `s` | Cycle sort column (`Updated β ID β Milestone β Title β β¦`) |
| `S` | Toggle sort direction (ascending / descending) |
| `Space` | **Toggle flag β
** on the selected MR β persisted across restarts |
| `F` | Open filter picker (state, mergeability, notes, milestone, assigneeβ¦) |
| `Del` | Delete selected MR row |
| `Esc` | Quit dashboard |
### π© Column Picker Mode
Opened with `C`. The table border turns **cyan** as a visual indicator.
| `β²` / `βΌ` or `k` / `j` | Navigate the column list |
| `Space` | Toggle the highlighted column on/off |
| `Enter` or `Esc` | Close the picker β changes are saved immediately to `config.json` |
### π¨ Insert Mode
The input field has exclusive focus. All printable keys feed the field β shortcuts are suspended. The input bar turns **yellow** as a visual indicator.
| `142` + `Enter` | Add MR ID `!142` to tracking |
| `staging` + `Enter` | Add branch `staging` to target columns |
| `-142` + `Enter` | Remove MR ID `!142` from tracking |
| `-staging` + `Enter` | Remove branch column `staging` |
| `@name` | Filter milestones matching `name` β opens autocomplete dropdown |
| `Enter` | Submit input, or confirm highlighted milestone suggestion |
| `Esc` | Close autocomplete dropdown, or cancel and return to Normal mode |
#### π Milestone Autocomplete (Insert Mode)
When the input starts with `@`, a dropdown appears above the input bar listing all active/upcoming milestones fetched from GitLab. The list is filtered in real time as you type.
| `β` / `β` or `Shift+Tab` / `Tab` | Navigate suggestions |
| `Enter` | Confirm selection β bulk-adds all open MRs from the milestone |
| `Esc` | Close dropdown without selecting |
> **Why two modes?** Branch names starting with `s`, `S`, `p`, `P`, `o`, `O`, `r` or `R` would otherwise collide with shortcut keys. Insert mode guarantees the full branch name is captured without interference.
---
## ποΈ Project Architecture
This project is structured as a **Cargo workspace** with four crates:
```text
gitlab-tracker/ # Binary crate β TUI orchestrator
βββ src/
βββ main.rs # Entry point: wires providers, calls build_tracker_colors(), event loop
βββ app.rs # State machine, InputMode, row navigation & sort logic
βββ config.rs # Label filtering, wildcard matching, parse_color() & activity badge
βββ models.rs # Strongly-typed API DTOs & runtime event types
βββ gitlab.rs # Async network handling & rate-limit semaphores
βββ events.rs # Keyboard & mouse event dispatch (Normal / Insert mode routing)
βββ storage.rs # OS Keyring interface & XDG state/config persistence
βββ utils.rs # Fuzzy matching algorithmic utilities
βββ demo.rs # Demo mode with pre-populated mock data (screenshots & CI)
βββ ui/
βββ mod.rs # Root layout renderer & input bar (mode-aware)
βββ table.rs # Main MR table widget
βββ inspector.rs # Upper-right pane: MR metadata & pipeline history
βββ tracker.rs # Lower-right pane: linked ticket details & time log (TrackerLabelColors)
gitlab-tracker-core/ # Library crate β shared trait contracts, zero UI dependency
βββ src/
βββ lib.rs # Re-exports: TrackerProvider, LinkedTicket, LabelColorMaps, β¦
βββ provider.rs # TrackerProvider trait + all shared domain types
# LinkedTicket: flat ticket data (type, priority, version, progress, β¦)
# LabelColorMaps: raw (String, String) badge colour maps β no ratatui
gitlab-tracker-notify/ # Library crate β optional desktop notification plugin
βββ src/
βββ lib.rs # notify-rust integration (no-op stubs when feature `desktop` is disabled)
gitlab-tracker-redmine/ # Library crate β optional Redmine integration plugin
βββ src/
βββ lib.rs # RedmineProvider: implements TrackerProvider + label_colors()
βββ client.rs # Async Redmine REST API client (issue, time entries, activities)
βββ config.rs # RedmineConfig: YAML load/save, LabelColorConfig, onboarding prompt
βββ detector.rs # Regex-based ticket ID detector (title & description)
βββ keyring.rs # Secure token retrieval via OS Keyring
```
### Optional Feature Flags
| `notifications` | β
enabled | Desktop notifications via `notify-rust` |
| `redmine` | β disabled | Redmine ticket & time-tracking integration (see [`gitlab-tracker-redmine`](gitlab-tracker-redmine/README.md)) |
### Tracker Plugins
`gitlab-tracker` supports optional external tracker integrations (Redmine, and future providers such as Jira or Linear) through a plugin architecture based on the `TrackerProvider` trait defined in `gitlab-tracker-core`.
When a tracker plugin is configured, the dashboard is enriched with:
* **Linked ticket display** in the Inspector β subject, type, priority, status, assignee, target version, start date, progress bar, and time tracking (estimate / spent / remaining)
* **Coloured badges** for Type and Priority β colours are fully configurable per-label in the plugin's config file (no hardcoded values β works with any language or custom workflow)
* **Time Log view** (`P`) β time entries for the linked ticket, auto-refreshed on MR navigation
* **Log time** (`L`) β submit a new time entry directly from the TUI
* **Tracker column** in the table (toggleable via `C`)
Each plugin lives in its own crate and is activated via a Cargo feature flag. See the plugin's own README for setup instructions:
| **Redmine** | `--features redmine` | [`gitlab-tracker-redmine/README.md`](gitlab-tracker-redmine/README.md) |
---
## π License
Distributed under the MIT License. See `LICENSE` for details.