gitlab-tracker-redmine 0.3.4

Optional Redmine integration plugin for gitlab-tracker
docs.rs failed to build gitlab-tracker-redmine-0.3.4
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

gitlab-tracker-redmine

Optional Redmine integration plugin for gitlab-tracker.

Implements the TrackerProvider trait from gitlab-tracker-core to detect Redmine ticket references in MR titles and descriptions, and enrich the TUI with ticket details and time tracking.


Features

  • Linked ticket display in the Inspector's MR Info view — the following fields are shown when available:

    Field Source (GET /issues/{id}.json)
    Ticket ID + Subject id, subject
    Type badge tracker.name (e.g. "Bug", "Evolution")
    Priority badge priority.name (e.g. "Normal", "High")
    Status status.name
    Author / Assignee author.name, assigned_to.name
    Target version fixed_version.name
    Start date start_date
    Progress bar done_ratio (0–100 %)
    Estimate / Spent / Remaining estimated_hours, spent_hours, remaining_hours
    Direct URL built from redmine_url + ticket ID

    Type and Priority are rendered as coloured badges — colours are fully configurable in redmine.yaml (see Configuration below).

  • Time Log view (P): press P twice to reach the Time Log view:

    • A progress bar comparing time spent vs. the ticket's estimate.
    • The full list of time entries (date, user, activity, duration, comment) fetched live from Redmine.
    • Navigating between MRs with / while on this view automatically refreshes the entries for the newly selected ticket.
  • Log time (L): open a popup to submit a new time entry directly to Redmine — select the activity category, enter a duration (e.g. 1h30, 90m, 1.5h), optionally add a comment, and confirm with Enter.

  • Tracker column in the main table (toggleable via C) — shows ticket ID, status, and spent/estimated time at a glance.


Enabling at Build Time

The redmine feature flag must be explicitly passed when building or installing:

# Build from source
cargo build --release --features redmine

# Install from source
cargo install --path gitlab-tracker --features redmine

# Install from crates.io
cargo install gitlab-tracker --features redmine

Without the flag, this crate is not compiled and there is zero runtime overhead.


Configuration

On first launch with the feature enabled, the app interactively prompts for your Redmine URL:

🌐 No Redmine URL found in config or environment.
   Leave empty to disable Redmine integration.
Redmine URL: https://redmine.my-company.com

Leaving the prompt empty silently disables the integration — no error, no impact on the rest of the dashboard.

The configuration is persisted as a YAML file:

  • Linux: ~/.config/gitlab-tracker/gitlab-tracker/redmine.yaml
  • macOS: ~/Library/Application Support/gitlab-tracker/gitlab-tracker/redmine.yaml
  • Windows: C:\Users\<User>\AppData\Roaming\gitlab-tracker\gitlab-tracker\redmine.yaml

You can also pre-configure via environment variable to skip the prompt entirely:

REDMINE_URL=https://redmine.my-company.com

redmine.yaml — full reference

# ── Connection ────────────────────────────────────────────────────────────────

# Base URL of your Redmine instance. No trailing slash.
# Can also be set via the REDMINE_URL environment variable.
redmine_url: "https://redmine.my-company.com"

# ── Ticket detection ──────────────────────────────────────────────────────────

# Regex patterns used to detect ticket IDs in MR titles and descriptions.
# Each pattern must expose the numeric ID in capture group 1.
# The first match wins; patterns are tried in order.
ticket_patterns:
  - "#(\\d+)"                                        # plain #1234
  - "(?i)(?:refs|fixes|closes|resolves)\\s+#(\\d+)" # refs #1234, fixes #1234, …
  - "/issues/(\\d+)"                                 # full Redmine URL in description

# ── Badge colours ─────────────────────────────────────────────────────────────
#
# Colour both the "Type" and "Priority" badges displayed in the Inspector panel.
# Keys are matched CASE-INSENSITIVELY against the value returned by the Redmine API.
# Use "*" as a catch-all fallback for any label not listed explicitly.
#
# Accepted colour values:
#   Named:  red, green, yellow, blue, magenta, cyan, white, black, dark_gray, light_gray
#   Hex:    #rrggbb  (e.g. #ff6600)

# tracker_type_colors maps the "tracker.name" field (ticket category in Redmine).
# Discover your instance's values with:
#   curl -s -H "X-Redmine-API-Key: YOUR_TOKEN" \
#     "https://your-redmine.com/issues/ANY_ID.json" | jq '.issue.tracker'
tracker_type_colors:
  "Bug":       { bg: "red",      fg: "white" }
  "Evolution": { bg: "cyan",     fg: "black" }
  "Support":   { bg: "yellow",   fg: "black" }
  "*":         { bg: "dark_gray", fg: "white" } # catch-all for unlisted types

# priority_colors maps the "priority.name" field.
# Discover your instance's values with:
#   curl -s -H "X-Redmine-API-Key: YOUR_TOKEN" \
#     "https://your-redmine.com/issues/ANY_ID.json" | jq '.issue.priority'
priority_colors:
  "Low":   { bg: "dark_gray", fg: "white" }
  "Normal": { bg: "dark_gray", fg: "white" }
  "High":   { bg: "yellow",    fg: "black" }
  "Urgent": { bg: "red",       fg: "white" }
  "*":       { bg: "dark_gray", fg: "white" } # catch-all for unlisted priorities
Field Required Description
redmine_url Base URL of your Redmine instance (no trailing slash)
ticket_patterns Regex list to detect ticket IDs in MR titles/descriptions — capture group 1 must match the numeric ID
tracker_type_colors Badge colour map for the tracker.name field. Keys are case-insensitive; "*" is a catch-all fallback. Omit the section entirely to use the default (dark_gray / white)
priority_colors Badge colour map for the priority.name field. Same rules as above

How to discover your Redmine's label values

Label names (tracker types, priorities) are instance-specific and may be in any language. Run these commands against any existing issue to see what your instance returns:

# Tracker type and priority of issue #1234
curl -s -H "X-Redmine-API-Key: YOUR_TOKEN" \
  "https://your-redmine.com/issues/1234.json" \
  | jq '.issue | {tracker: .tracker.name, priority: .priority.name}'

# All priorities defined in your instance
curl -s -H "X-Redmine-API-Key: YOUR_TOKEN" \
  "https://your-redmine.com/enumerations/issue_priorities.json" \
  | jq '[.issue_priorities[].name]'

Estimate to Complete (ETC) — automatic update on time entry submission

When you log time via the L popup, the app automatically recomputes the Estimate to Complete (ETC) on the linked Redmine ticket and writes it back.

ETC (Estimate to Complete) is the standard project management term for the remaining effort needed to finish a task. It is sometimes labelled Remaining time or Reste à faire (RAF) in French Redmine instances.

How it works

The computation uses fields returned directly by the Redmine issue API (GET /issues/{id}.json) — no admin access required:

Priority Field used Formula Available on
1 (preferred) remaining_hours remaining_hours − new_hours Redmine instances with a budget/planning plugin
2 (fallback) estimated_hours + spent_hours estimated_hours − spent_hours − new_hours All standard Redmine instances

Both strategies clamp the result to 0.0 — ETC cannot be negative.

The ETC and budget are submitted directly on the time entry (POST /time_entries.json), which is how the Redmine Budget plugin tracks them. The remaining_hours field on the issue is then automatically recalculated by the plugin — no separate write to the issue is needed. If your Redmine instance does not expose these fields, they are simply omitted from the payload — the time entry is still created successfully.

No configuration needed

The ETC update is fully automatic and requires no changes to redmine.yaml. It activates whenever the issue returns usable time fields, and degrades gracefully otherwise.

Verify your instance exposes the fields by running:

curl -s -H "X-Redmine-API-Key: YOUR_TOKEN" \
  "https://your-redmine.com/issues/YOUR_ISSUE_ID.json" \
  | jq '.issue | {estimated_hours, spent_hours, remaining_hours}'

If remaining_hours appears in the output, Strategy 1 is used. If only estimated_hours and spent_hours appear, Strategy 2 (fallback) is used.


API Token

The Redmine personal API token follows the same secure lookup chain as the GitLab token — it is never stored in plain text:

1. REDMINE_TOKEN environment variable (if set)
2. Native OS Keyring (GNOME Keyring / macOS Keychain / Windows Credential Manager)
3. Interactive CLI prompt → saved to OS Keyring

Adding a New Tracker Plugin

To add a different tracker (Jira, Linear, …), create a new crate and implement the TrackerProvider trait from gitlab-tracker-core.

Required methods

#[async_trait]
impl TrackerProvider for MyProvider {
    fn name(&self) -> &'static str { "My Tracker" }

    fn detect_ticket_id(&self, title: &str, description: &str) -> Option<String> { ... }

    async fn fetch_ticket(&self, ticket_id: &str) -> Option<LinkedTicket> { ... }

    fn ticket_url(&self, ticket_id: &str) -> String { ... }
}

Optional overrides (all have default no-op implementations)

    // Badge colours for Type and Priority labels — read from your config file.
    // Return raw (bg, fg) string pairs; the orchestrator converts them to ratatui::Color.
    // Omit to use the hard-coded fallback (dark_gray / white).
    fn label_colors(&self) -> LabelColorMaps { ... }

    // Time-tracking support:
    async fn fetch_activities(&self) -> Vec<Activity> { ... }
    async fn fetch_time_entries(&self, ticket_id: &str) -> Vec<TimeEntry> { ... }
    async fn log_time(&self, ticket_id: &str, entry: TimeEntryRequest) -> Result<(), String> { ... }

Wiring in main.rs

Add a #[cfg(feature = "my-tracker")] block in gitlab-tracker/src/main.rs — the build_tracker_colors helper is already generic over any TrackerProvider, so no further changes are needed in the orchestrator:

#[cfg(feature = "my-tracker")]
if let Some(ref provider) = my_tracker_provider {
    app.tracker_colors = build_tracker_colors(provider.as_ref());
    app.tracker = my_tracker_provider;
}

The LabelColorMaps flow:

Your config file (YAML/JSON/…)
  └─ label_colors() in your provider   ← String pairs, no ratatui dependency
       └─ build_tracker_colors()        ← converts to ratatui::Color (orchestrator only)
            └─ TrackerLabelColors       ← passed to the Inspector renderer