oxy show --stat: the files a change touched, with lines added and removed 0299076f09a2
Metal Gear grepped the diff to see which files a change touched. --stat lists them instead of the diff (path, +added -removed, kind; renames as from → to) with a total, and --json gives each file's added and removed counts. Closes #119
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key f89978fd095c
- When
- · 2026-10-18 16:40 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
0299076f09a24fd7af396c58f21b06a7bba19415e9b6f78e20ca8d1a1aa58475- Depends on
- 2c9ddac3c387 50a046e439e9 75167672ebcd 94084087a876 c6aa5bd9d1c8 d42369abf180 fce44ae9ad29 ff0af1ee299d
- Depended on by
- 87322662f607
- In bookmarks
- doctor-trust identical-sides keyless-tags main paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state
- In tags
- v0.10.0 v0.11.0 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 16 atoms, 0 tree operations, 4 files
Browse the tree at this change
modified docs/spec.md +1 −1
| … | 873 unchanged lines |
||
| 874 | 874 | oxy init [PATH] [--bare] oxy clone URL [--sparse PATH].. [--depth N] [--max-blob SIZE] |
|
| 875 | 875 | oxy status [--json] oxy diff oxy prompt [--format F] |
|
| 876 | 876 | oxy commit -m MSG [--depends-on CHANGE].. |
|
| 877 | - | oxy log [-r REVSET] [-n N] [-v] [-L S,E:PATH] oxy show CHANGE | -r REVSET |
|
| 877 | + | oxy log [-r REVSET] [-n N] [-v] [-L S,E:PATH] oxy show CHANGE | -r REVSET [--stat] |
|
| 878 | 878 | oxy describe [CHANGE] [-m MSG] oxy amend [-m MSG] [--into CHANGE] |
|
| 879 | 879 | oxy split CHANGE [PATH].. oxy abandon CHANGE | -r REVSET [--yes] oxy resolve |
|
| 880 | 880 | oxy checkout [CHANGE].. [--discard|--carry] oxy undo oxy op log |
|
| … | 165 unchanged lines |
modified src/cli/mod.rs +5 −1
| … | 264 unchanged lines |
||
| 265 | 265 | /// Show every change a revset selects |
|
| 266 | 266 | #[arg(short = 'r', long, value_name = "REVSET", conflicts_with = "change")] |
|
| 267 | 267 | revset: Option<String>, |
|
| 268 | + | /// The files it touched with lines added and removed, instead of the diff |
|
| 269 | + | #[arg(long)] |
|
| 270 | + | stat: bool, |
|
| 268 | 271 | /// Machine-readable output: one JSON document on stdout |
|
| 269 | 272 | #[arg(long)] |
|
| 270 | 273 | json: bool, |
|
| … | 844 unchanged lines |
||
| 1115 | 1118 | Cmd::Show { |
|
| 1116 | 1119 | change, |
|
| 1117 | 1120 | revset, |
|
| 1121 | + | stat, |
|
| 1118 | 1122 | json, |
|
| 1119 | - | } => workspace::show(change.as_deref(), revset.as_deref(), json), |
|
| 1123 | + | } => workspace::show(change.as_deref(), revset.as_deref(), stat, json), |
|
| 1120 | 1124 | Cmd::Checkout { |
|
| 1121 | 1125 | changes, |
|
| 1122 | 1126 | discard, |
|
| … | 233 unchanged lines |
modified src/cli/workspace.rs +62 −12
| … | 1032 unchanged lines |
||
| 1033 | 1033 | Ok(0) |
|
| 1034 | 1034 | } |
|
| 1035 | 1035 | ||
| 1036 | - | pub fn show(change: Option<&str>, revset: Option<&str>, as_json: bool) -> Result<i32> { |
|
| 1036 | + | pub fn show(change: Option<&str>, revset: Option<&str>, stat: bool, as_json: bool) -> Result<i32> { |
|
| 1037 | 1037 | let repo = open()?; |
|
| 1038 | 1038 | repo.snapshot_status("oxy show")?; |
|
| 1039 | 1039 | let Some(r) = revset else { |
|
| 1040 | 1040 | let h = repo.resolve_change(change.unwrap_or("@"))?; |
|
| 1041 | - | return show_one(&repo, h, as_json, false); |
|
| 1041 | + | return show_one(&repo, h, as_json, false, stat); |
|
| 1042 | 1042 | }; |
|
| 1043 | 1043 | // Several changes: a JSON array, or the changes one after another. |
|
| 1044 | 1044 | let hashes = repo.revset(r)?.hashes; |
|
| … | 3 unchanged lines |
||
| 1048 | 1048 | if i > 0 { |
|
| 1049 | 1049 | print!(","); |
|
| 1050 | 1050 | } |
|
| 1051 | - | show_one(&repo, *h, true, true)?; |
|
| 1051 | + | show_one(&repo, *h, true, true, stat)?; |
|
| 1052 | 1052 | } |
|
| 1053 | 1053 | println!("]"); |
|
| 1054 | 1054 | return Ok(0); |
|
| … | 6 unchanged lines |
||
| 1061 | 1061 | if i > 0 { |
|
| 1062 | 1062 | println!(); |
|
| 1063 | 1063 | } |
|
| 1064 | - | show_one(&repo, *h, false, false)?; |
|
| 1064 | + | show_one(&repo, *h, false, false, stat)?; |
|
| 1065 | 1065 | } |
|
| 1066 | 1066 | Ok(0) |
|
| 1067 | 1067 | } |
|
| 1068 | 1068 | ||
| 1069 | + | /// Lines added and removed in one file's diff. |
|
| 1070 | + | fn counts(f: &oxy_core::repo::FileDiff) -> (usize, usize) { |
|
| 1071 | + | f.hunks |
|
| 1072 | + | .iter() |
|
| 1073 | + | .fold((0, 0), |(a, r), h| (a + h.added.len(), r + h.removed.len())) |
|
| 1074 | + | } |
|
| 1075 | + | ||
| 1076 | + | /// What `--stat` prints (oxygen#119): each file the change touched, its |
|
| 1077 | + | /// lines added and removed, then the total. |
|
| 1078 | + | fn print_stat(files: &[oxy_core::repo::FileDiff]) { |
|
| 1079 | + | let width = files |
|
| 1080 | + | .iter() |
|
| 1081 | + | .map(|f| f.path.chars().count()) |
|
| 1082 | + | .max() |
|
| 1083 | + | .unwrap_or(0) |
|
| 1084 | + | .min(60); |
|
| 1085 | + | let (mut added, mut removed) = (0, 0); |
|
| 1086 | + | for f in files { |
|
| 1087 | + | let (a, r) = counts(f); |
|
| 1088 | + | added += a; |
|
| 1089 | + | removed += r; |
|
| 1090 | + | let path = match &f.from { |
|
| 1091 | + | Some(from) => format!("{from} → {}", f.path), |
|
| 1092 | + | None => f.path.clone(), |
|
| 1093 | + | }; |
|
| 1094 | + | println!( |
|
| 1095 | + | " {path:<width$} {} {} {}", |
|
| 1096 | + | ui::ok(format!("{:>6}", format!("+{a}"))), |
|
| 1097 | + | ui::err(format!("{:>6}", format!("-{r}"))), |
|
| 1098 | + | ui::dim(&f.kind) |
|
| 1099 | + | ); |
|
| 1100 | + | } |
|
| 1101 | + | println!( |
|
| 1102 | + | " {}", |
|
| 1103 | + | ui::dim(format!( |
|
| 1104 | + | "{} changed, {added} added, {removed} removed", |
|
| 1105 | + | plural(files.len(), "file") |
|
| 1106 | + | )) |
|
| 1107 | + | ); |
|
| 1108 | + | } |
|
| 1109 | + | ||
| 1069 | 1110 | /// One change, as `oxy show` prints it; `inline` JSON goes without its |
|
| 1070 | - | /// newline, for an array. |
|
| 1071 | - | fn show_one(repo: &Repo, h: ChangeHash, as_json: bool, inline: bool) -> Result<i32> { |
|
| 1111 | + | /// newline, for an array. `stat` lists the files instead of the diff. |
|
| 1112 | + | fn show_one(repo: &Repo, h: ChangeHash, as_json: bool, inline: bool, stat: bool) -> Result<i32> { |
|
| 1072 | 1113 | let c = repo.store().get_change(&h)?; |
|
| 1073 | 1114 | if as_json { |
|
| 1074 | 1115 | let files = repo.diff_files(Some(&h))?; |
|
| … | 9 unchanged lines |
||
| 1084 | 1125 | body.push_str(&format!( |
|
| 1085 | 1126 | ",{}:{}}}", |
|
| 1086 | 1127 | json::s("files"), |
|
| 1087 | - | json::arr(files.iter().map(|f| json::obj([ |
|
| 1088 | - | ("path", json::s(&f.path)), |
|
| 1089 | - | ("kind", json::s(&f.kind)), |
|
| 1090 | - | ("from", json::opt(f.from.as_deref(), json::s)) |
|
| 1091 | - | ]))) |
|
| 1128 | + | json::arr(files.iter().map(|f| { |
|
| 1129 | + | let (a, r) = counts(f); |
|
| 1130 | + | json::obj([ |
|
| 1131 | + | ("path", json::s(&f.path)), |
|
| 1132 | + | ("kind", json::s(&f.kind)), |
|
| 1133 | + | ("from", json::opt(f.from.as_deref(), json::s)), |
|
| 1134 | + | ("added", a.to_string()), |
|
| 1135 | + | ("removed", r.to_string()), |
|
| 1136 | + | ]) |
|
| 1137 | + | })) |
|
| 1092 | 1138 | )); |
|
| 1093 | 1139 | if inline { |
|
| 1094 | 1140 | print!("{body}"); |
|
| … | 52 unchanged lines |
||
| 1147 | 1193 | println!(" {line}"); |
|
| 1148 | 1194 | } |
|
| 1149 | 1195 | println!(); |
|
| 1150 | - | print!("{}", repo.describe_change(&h)?); |
|
| 1196 | + | if stat { |
|
| 1197 | + | print_stat(&repo.diff_files(Some(&h))?); |
|
| 1198 | + | } else { |
|
| 1199 | + | print!("{}", repo.describe_change(&h)?); |
|
| 1200 | + | } |
|
| 1151 | 1201 | Ok(0) |
|
| 1152 | 1202 | } |
|
| 1153 | 1203 | ||
| … | 1382 unchanged lines |
modified tests/cli_json.rs +9 −0
| … | 163 unchanged lines |
||
| 164 | 164 | assert_eq!(by_path["changes"].as_array().unwrap().len(), 2, "{by_path}"); |
|
| 165 | 165 | let show = json(r, &home, &["show", "--json", &format!("#{}", &hash[..12])]); |
|
| 166 | 166 | assert_eq!(show["files"][0]["path"], "b.txt"); |
|
| 167 | + | // oxygen#119: lines added and removed per file, and --stat. |
|
| 168 | + | assert!(show["files"][0]["added"].as_u64().unwrap() >= 1, "{show}"); |
|
| 169 | + | let (code, out, _) = oxy(r, &home, &["show", "--stat", &format!("#{}", &hash[..12])]); |
|
| 170 | + | let out = String::from_utf8_lossy(&out); |
|
| 171 | + | assert!( |
|
| 172 | + | code == 0 && out.contains("b.txt") && out.contains("1 file changed"), |
|
| 173 | + | "{out}" |
|
| 174 | + | ); |
|
| 175 | + | assert!(!out.contains("@@"), "the files, not the diff: {out}"); |
|
| 167 | 176 | let verify = json(r, &home, &["verify", "--json"]); |
|
| 168 | 177 | assert!(verify |
|
| 169 | 178 | .as_array() |
|
| … | 454 unchanged lines |