`why` no longer hands out commit authors' email addresses. Why a line is the way it is (API why, MCP repository why) returned the blamed commit as git stores it, author address included, to anyone who can read the repository. Its commit is now a WhyCommit: the author is named as git wrote them and by `username`, the g1t account their address belongs to (a confirmed or noreply address, or g1t's own), as list_commits already does, and the address is never returned. The guide, the operation's description, its example and the OpenAPI document say so. Checked: cargo test for g1t-contracts (new: an author is named by account, never by address) and g1t-api (204, the OpenAPI document regenerated), the CLI's (149), cargo check --workspace and g1t-api's wasm32 build.
6 files+67−60/6 viewed
| 1529 | 1529 | "The decision records on an issue (with those on its pull requests) or on a pull request, oldest first: each with its kind (proposal, claim, integration_failure, integration_passed, fix, approval, merge or note), its confidence (claimed: someone said so; tested: a check or a combination of pull requests proved it; approved: a person approved it), who recorded it (absent for g1t), its summary and its evidence. Issues and pull requests share numbers, so number names either." | |
| 1530 | 1530 | } | |
| 1531 | 1531 | Op::Why => { | |
| 1532 | − | "Why a line of a file is the way it is: the commit that last changed it (by blame, at ref or the default branch), the merged pull request it arrived in, that pull request's issue, the decision records on the pull request, and the session entries recorded around that commit (what was said and the tool calls that touched the file, at most eight). pull, issue, decisions and sessions are empty when the commit did not arrive through a pull request." | |
| 1532 | + | "Why a line of a file is the way it is: the commit that last changed it (by blame, at ref or the default branch; its author named as git wrote them, with `username`, the g1t account their address belongs to, when one does, and never the address), the merged pull request it arrived in, that pull request's issue, the decision records on the pull request, and the session entries recorded around that commit (what was said and the tool calls that touched the file, at most eight). pull, issue, decisions and sessions are empty when the commit did not arrive through a pull request." | |
| 1533 | 1533 | } | |
| 1534 | 1534 | Op::MarkPullRequestReady => { | |
| 1535 | 1535 | "Mark a draft pull request ready for review. Push your commits first. The summary becomes its description and should say what changed and why." |
| 3143 | 3143 | "message": "Read retry_limit, as #12 names it", | |
| 3144 | 3144 | "author": { | |
| 3145 | 3145 | "name": "Ana Lima", | |
| 3146 | − | "email": "ana@example.com" | |
| 3146 | + | "username": "ana" | |
| 3147 | 3147 | }, | |
| 3148 | 3148 | "parents": [ | |
| 3149 | 3149 | "4be1c0d9a2f3e4b5c6d7e8f90a1b2c3d4e5f6a7b" |
| 7 | 7 | //! request's issue, its decision records, and the session entries recorded | |
| 8 | 8 | //! around the commit. | |
| 9 | 9 | ||
| 10 | − | use g1t_contracts::decisions::{Decision, ListDecisionsArgs, Why, WhyIssue, WhyPull}; | |
| 10 | + | use g1t_contracts::decisions::{Decision, ListDecisionsArgs, Why, WhyCommit, WhyIssue, WhyPull}; | |
| 11 | 11 | use g1t_contracts::repos::{Blame, BlameArgs, Commit, LogArgs, RepoPath}; | |
| 12 | 12 | use g1t_contracts::work::{IssueDetail, ListPullsArgs, Pull, PullStatus, SessionEntry, SessionEntryKind, State, ViewArgs}; | |
| 13 | 13 | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| ⋯ | |||
| 143 | 143 | Outcome::Ok(blame) => blame, | |
| 144 | 144 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 145 | 145 | }; | |
| 146 | − | let Some(commit) = commit_of_line(&blame, line).cloned() else { | |
| 146 | + | let Some(found) = commit_of_line(&blame, line).cloned() else { | |
| 147 | 147 | let lines = blame.ranges.last().map_or(0, |range| range.end); | |
| 148 | 148 | return Ok(Outcome::fail( | |
| 149 | 149 | FailureCode::Invalid, | |
| ⋯ | |||
| 168 | 168 | Outcome::Ok(pulls) => pulls, | |
| 169 | 169 | Outcome::Fail(_) => Vec::new(), | |
| 170 | 170 | }; | |
| 171 | + | // Who wrote it, by account: the address itself is never returned. | |
| 172 | + | let authors = crate::commits::read_authors(services, &[&found]).await; | |
| 173 | + | let username = crate::commits::username(&found, &authors).map(str::to_owned); | |
| 174 | + | let commit = WhyCommit::of(found, username); | |
| 171 | 175 | let mut pull = pull_for_commit(&merged, &[], &commit.hash).cloned(); | |
| 172 | 176 | if pull.is_none() && !merged.is_empty() { | |
| 173 | 177 | let log: Outcome<Vec<Commit>> = g1t_kit::call( | |
| 108 | 108 | ||
| 109 | 109 | | Field | | | |
| 110 | 110 | | --- | --- | | |
| 111 | − | | `commit` | The commit that last changed the line: `hash`, `message`, `author`, `parents`, `authored_at`. | | |
| 111 | + | | `commit` | The commit that last changed the line: `hash`, `message`, `author` (`name` as git wrote it, and `username`, the g1t account the author's address belongs to, when one does; never the address), `parents`, `authored_at`. | | |
| 112 | 112 | | `pull` | The merged pull request it arrived in: `number`, `title`, `author`, `agent`, `merged_by`, `merged_at`. Absent when it did not arrive through one. | | |
| 113 | 113 | | `issue` | That pull request's issue: `number`, `title`, `state` and `body` (cut at 900 characters). | | |
| 114 | 114 | | `decisions` | The [decision records](#decision-records) on the pull request, oldest first. | |
Binary or large file; its contents are not shown.
| 231 | 231 | pub body: String, | |
| 232 | 232 | } | |
| 233 | 233 | ||
| 234 | + | /// The commit `why` names. Its author is named as git wrote them and by the | |
| 235 | + | /// g1t account their address belongs to, when one does; the address itself | |
| 236 | + | /// is never shown. | |
| 237 | + | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 238 | + | #[serde(rename_all = "camelCase")] | |
| 239 | + | pub struct WhyCommit { | |
| 240 | + | pub hash: String, | |
| 241 | + | pub tree_hash: String, | |
| 242 | + | pub message: String, | |
| 243 | + | pub author: WhyAuthor, | |
| 244 | + | pub parents: Vec<String>, | |
| 245 | + | /// RFC 3339. | |
| 246 | + | pub authored_at: String, | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | /// Who wrote a commit, without their address. | |
| 250 | + | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 251 | + | pub struct WhyAuthor { | |
| 252 | + | pub name: String, | |
| 253 | + | /// The g1t account the author's address belongs to: a confirmed or | |
| 254 | + | /// noreply address of one, or g1t's own. | |
| 255 | + | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 256 | + | pub username: Option<String>, | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | impl WhyCommit { | |
| 260 | + | /// `commit` as `why` shows it, its author's account `username` when known. | |
| 261 | + | pub fn of(commit: Commit, username: Option<String>) -> WhyCommit { | |
| 262 | + | WhyCommit { | |
| 263 | + | hash: commit.hash, | |
| 264 | + | tree_hash: commit.tree_hash, | |
| 265 | + | message: commit.message, | |
| 266 | + | author: WhyAuthor { name: commit.author.name, username }, | |
| 267 | + | parents: commit.parents, | |
| 268 | + | authored_at: commit.authored_at, | |
| 269 | + | } | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | ||
| 234 | 273 | /// `why`: why a line is the way it is. The commit that last changed it, | |
| 235 | 274 | /// the merged pull request it arrived in, that pull request's issue, the | |
| 236 | 275 | /// decisions recorded on them, and the session entries behind the commit. | |
| ⋯ | |||
| 239 | 278 | pub struct Why { | |
| 240 | 279 | pub path: String, | |
| 241 | 280 | pub line: u32, | |
| 242 | − | pub commit: Commit, | |
| 281 | + | pub commit: WhyCommit, | |
| 243 | 282 | pub pull: Option<WhyPull>, | |
| 244 | 283 | pub issue: Option<WhyIssue>, | |
| 245 | 284 | pub decisions: Vec<Decision>, | |
| ⋯ | |||
| 251 | 290 | use super::*; | |
| 252 | 291 | ||
| 253 | 292 | #[test] | |
| 293 | + | fn why_names_an_author_by_account_never_by_address() { | |
| 294 | + | let commit = Commit { | |
| 295 | + | hash: "7d3e".into(), | |
| 296 | + | tree_hash: "c1d2".into(), | |
| 297 | + | message: "Read retry_limit".into(), | |
| 298 | + | author: crate::repos::Signature { name: "Chase Pierce".into(), email: "syntaqx@gmail.com".into() }, | |
| 299 | + | parents: vec![], | |
| 300 | + | authored_at: "2026-10-14T00:00:00Z".into(), | |
| 301 | + | }; | |
| 302 | + | let known = serde_json::to_value(WhyCommit::of(commit.clone(), Some("syntaqx".into()))).unwrap(); | |
| 303 | + | assert_eq!(known["author"], serde_json::json!({ "name": "Chase Pierce", "username": "syntaqx" })); | |
| 304 | + | let unknown = serde_json::to_value(WhyCommit::of(commit, None)).unwrap(); | |
| 305 | + | assert_eq!(unknown["author"], serde_json::json!({ "name": "Chase Pierce" })); | |
| 306 | + | assert!(!known.to_string().contains("gmail") && !unknown.to_string().contains("gmail")); | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | ||
| 310 | + | #[test] | |
| 254 | 311 | fn kinds_read_and_write_as_their_names() { | |
| 255 | 312 | for kind in [ | |
| 256 | 313 | DecisionKind::Proposal, | |
Comments
Sign in to comment.