OXYGENHUB

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