gitway_lib/diagnostic.rs
1// SPDX-License-Identifier: GPL-3.0-or-later
2//! Single-line failure diagnostic for every Gitway binary.
3//!
4//! When a Gitway binary runs and fails in human (non-JSON) mode, one
5//! [`emit`] or [`emit_for`] call writes a logfmt-style record to stderr:
6//!
7//! ```text
8//! gitway diag ts=2026-04-22T18:43:11Z pid=12345 code=4 reason=PERMISSION_DENIED argv=["gitway", "git@github.com", "git-upload-pack", "'org/repo.git'"]
9//! ```
10//!
11//! The point is to turn silent `exit 128` failures — the opaque code git
12//! reports when `core.sshCommand` fails — into a single grep-able line
13//! that carries enough context to triage: ISO 8601 timestamp, PID, argv,
14//! exit code, and a short error reason.
15//!
16//! JSON mode already carries `timestamp` and `command` in its structured
17//! `{"error": {...}}` blob, so callers should skip this helper on that
18//! path. Stdout is always left untouched (SFRS Rule 1) — the diagnostic
19//! writes exclusively to stderr.
20
21use crate::error::GitwayError;
22use crate::time::now_iso8601;
23
24/// Emits the single-line diagnostic record with an explicit exit code and
25/// a reason string. Use this from the shim binaries (`gitway-keygen`,
26/// `gitway-add`) where the reason codes are selected from a local static
27/// table; use [`emit_for`] when a [`GitwayError`] is already in hand.
28pub fn emit(code: u32, reason: &str) {
29 let argv: Vec<String> = std::env::args().collect();
30 eprintln!(
31 "gitway diag ts={ts} pid={pid} code={code} reason={reason} argv={argv:?}",
32 ts = now_iso8601(),
33 pid = std::process::id(),
34 );
35}
36
37/// Emits the diagnostic record for a [`GitwayError`], reusing the error's
38/// mapped exit code and string error class.
39pub fn emit_for(err: &GitwayError) {
40 emit(err.exit_code(), err.error_code());
41}