Skip to content

Commit 2cbdeac

Browse files
feat(diag): M12.8 — config_source= field on gitway diag line (NFR-24) (#3)
Adds diagnostic::emit_for_with_config_sources(&AnvilError, &[PathBuf]) entry point. Emits the standard gitway diag line plus an additional config_source=path1,path2 field listing the ssh_config(5) files that were consulted during the failing invocation. An empty slice produces output identical to emit_for, so the new entry point is a strict superset. The Gitway CLI consumes this in a follow-up PR (M12.8 client side): gitway-cli/src/main.rs computes the deduplicated path list before calling run() and passes it to emit_for_with_config_sources at the existing emit_for(e) call site. Cargo.toml: 0.3.0 -> 0.3.1 (additive, no breaking changes). CHANGELOG: 0.3.1 entry. Plan: M12.8 of let-us-plan-on-bright-cosmos.md (Anvil server side).
1 parent 461e3ad commit 2cbdeac

4 files changed

Lines changed: 57 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to Anvil are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [SemVer](https://semver.org/).
44

5+
## [0.3.1] — 2026-05-04
6+
7+
### Added
8+
9+
- New `diagnostic::emit_for_with_config_sources(&AnvilError, &[PathBuf])` entry point for the M12.8 NFR-24 wiring. Emits the standard `gitway diag` line plus a `config_source=path1,path2` field listing the `ssh_config(5)` files that were consulted during the failing invocation. An empty slice produces output identical to [`emit_for`]. Existing `emit` / `emit_for` continue to work unchanged.
10+
511
## [0.3.0] — 2026-05-04
612

713
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SPDX-License-Identifier: GPL-3.0-or-later
22
[package]
33
name = "anvil-ssh"
4-
version = "0.3.0"
4+
version = "0.3.1"
55
edition = "2021"
66
license = "GPL-3.0-or-later"
77
authors = ["Mohamed Hammad <MJ@S3cure.me>"]

src/diagnostic.rs

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,79 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
2+
// Rust guideline compliant 2026-03-30
23
//! Single-line failure diagnostic for every Gitway binary.
34
//!
45
//! 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+
//! [`emit`] / [`emit_for`] / [`emit_for_with_config_sources`] call writes
7+
//! a logfmt-style record to stderr:
68
//!
79
//! ```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'"]
10+
//! gitway diag ts=2026-04-22T18:43:11Z pid=12345 code=4 reason=PERMISSION_DENIED config_source=~/.ssh/config,/etc/ssh/ssh_config argv=["gitway", "git@github.com", "git-upload-pack", "'org/repo.git'"]
911
//! ```
1012
//!
1113
//! The point is to turn silent `exit 128` failures — the opaque code git
1214
//! reports when `core.sshCommand` fails — into a single grep-able line
1315
//! that carries enough context to triage: ISO 8601 timestamp, PID, argv,
14-
//! exit code, and a short error reason.
16+
//! exit code, error reason, and (when relevant) the `ssh_config(5)`
17+
//! file(s) that were consulted (NFR-24, M12.8).
1518
//!
1619
//! JSON mode already carries `timestamp` and `command` in its structured
1720
//! `{"error": {...}}` blob, so callers should skip this helper on that
1821
//! path. Stdout is always left untouched (SFRS Rule 1) — the diagnostic
1922
//! writes exclusively to stderr.
2023
24+
use std::path::PathBuf;
25+
2126
use crate::error::AnvilError;
2227
use crate::time::now_iso8601;
2328

2429
/// Emits the single-line diagnostic record with an explicit exit code and
2530
/// a reason string. Use this from the shim binaries (`gitway-keygen`,
2631
/// `gitway-add`) where the reason codes are selected from a local static
27-
/// table; use [`emit_for`] when a [`AnvilError`] is already in hand.
32+
/// table; use [`emit_for`] when an [`AnvilError`] is already in hand.
2833
pub fn emit(code: u32, reason: &str) {
34+
emit_inner(code, reason, &[]);
35+
}
36+
37+
/// Emits the diagnostic record for an [`AnvilError`], reusing the error's
38+
/// mapped exit code and string error class.
39+
pub fn emit_for(err: &AnvilError) {
40+
emit_inner(err.exit_code(), err.error_code(), &[]);
41+
}
42+
43+
/// Like [`emit_for`], plus a `config_source=` field listing the
44+
/// `ssh_config(5)` files that were consulted during this invocation.
45+
///
46+
/// `config_sources` should be the deduplicated list of files the
47+
/// resolver attempted to read (typically `~/.ssh/config` and, on Unix,
48+
/// `/etc/ssh/ssh_config`). An empty slice produces a line identical to
49+
/// [`emit_for`] — no `config_source=` field is emitted.
50+
///
51+
/// This is the M12.8 entry point for NFR-24: callers that successfully
52+
/// or unsuccessfully consulted `ssh_config` should pass that fact down
53+
/// to the diagnostic so triage tooling can attribute behavior to the
54+
/// right file. The Gitway CLI does this around its top-level
55+
/// [`emit_for`]-equivalent call site (`gitway-cli/src/main.rs`).
56+
pub fn emit_for_with_config_sources(err: &AnvilError, config_sources: &[PathBuf]) {
57+
emit_inner(err.exit_code(), err.error_code(), config_sources);
58+
}
59+
60+
fn emit_inner(code: u32, reason: &str, config_sources: &[PathBuf]) {
2961
let argv: Vec<String> = std::env::args().collect();
62+
let extra = if config_sources.is_empty() {
63+
String::new()
64+
} else {
65+
let joined: Vec<String> = config_sources
66+
.iter()
67+
.map(|p| p.to_string_lossy().into_owned())
68+
.collect();
69+
// Field name is logfmt-style `key=value` like the others. The
70+
// value is a comma-separated list of paths; commas in paths are
71+
// exceedingly rare and not worth quoting for in this MVP.
72+
format!(" config_source={}", joined.join(","))
73+
};
3074
eprintln!(
31-
"gitway diag ts={ts} pid={pid} code={code} reason={reason} argv={argv:?}",
75+
"gitway diag ts={ts} pid={pid} code={code} reason={reason}{extra} argv={argv:?}",
3276
ts = now_iso8601(),
3377
pid = std::process::id(),
3478
);
3579
}
36-
37-
/// Emits the diagnostic record for a [`AnvilError`], reusing the error's
38-
/// mapped exit code and string error class.
39-
pub fn emit_for(err: &AnvilError) {
40-
emit(err.exit_code(), err.error_code());
41-
}

0 commit comments

Comments
 (0)