Skip to content

Commit 76268f8

Browse files
committed
More of a sketch of check_mode "translation" to Rust
All parts are included, but this is not yet believed to be correct.
1 parent 967b9df commit 76268f8

File tree

3 files changed

+89
-16
lines changed

3 files changed

+89
-16
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/it/Cargo.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ name = "it"
1414
path = "src/main.rs"
1515

1616
[dependencies]
17-
clap = { version = "4.5.16", features = ["derive"] }
1817
anyhow = "1.0.86"
19-
18+
clap = { version = "4.5.16", features = ["derive"] }
2019
gix = { version = "^0.68.0", path = "../../gix", default-features = false, features = ["attributes", "revision"] }
20+
once_cell = "1.20.2"
21+
regex = { version = "1.11.1", default-features = false, features = ["std"] }

tests/it/src/commands/check_mode.rs

+84-14
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,41 @@
11
pub(super) mod function {
22
use anyhow::{bail, Context};
33
use gix::bstr::ByteSlice;
4-
use std::ffi::OsString;
5-
use std::io::{BufRead, BufReader};
4+
use once_cell::sync::Lazy;
5+
use regex::bytes::Regex;
6+
use std::ffi::{OsStr, OsString};
7+
use std::io::{BufRead, BufReader, Read};
68
use std::process::{Command, Stdio};
79

810
pub fn check_mode() -> anyhow::Result<()> {
911
let root = find_root()?;
10-
let mut mismatch = false;
12+
let mut any_mismatch = false;
1113

12-
let cmd = Command::new("git")
13-
.arg("-C")
14-
.arg(root)
14+
let mut child = git_on(&root)
1515
.args(["ls-files", "-sz", "--", "*.sh"])
1616
.stdout(Stdio::piped())
1717
.spawn()
18-
.context("Can't run `git` to list index")?;
18+
.context("Can't start `git` subprocess to list index")?;
1919

20-
let stdout = cmd.stdout.expect("should have captured stdout");
21-
let reader = BufReader::new(stdout);
22-
for record in reader.split(b'\0') {
23-
// FIXME: Use the record, displaying messages and updating `mismatch`.
20+
let stdout = child.stdout.take().expect("should have captured stdout");
21+
for result in BufReader::new(stdout).split(b'\0') {
22+
let record = result.context(r"Can't read '\0'-terminated record")?;
23+
if check_for_mismatch(&root, &record)? {
24+
any_mismatch = true;
25+
}
2426
}
2527

26-
// FIXME: If `cmd` did not report successful completion, bail.
27-
// FIXME: If `mismatch` (any mismatches), bail.
28-
bail!("not yet implemented");
28+
let status = child.wait().context("Failure running `git` subprocess to list index")?;
29+
if !status.success() {
30+
bail!("`git` subprocess to list index did not complete successfully");
31+
}
32+
if any_mismatch {
33+
bail!("Mismatch found - scan completed, finding at least one `#!` vs. `+x` mismatch");
34+
}
35+
Ok(())
2936
}
3037

38+
/// Find the top-level directory of the current repository working tree.
3139
fn find_root() -> anyhow::Result<OsString> {
3240
let output = Command::new("git")
3341
.args(["rev-parse", "--show-toplevel"])
@@ -47,4 +55,66 @@ pub(super) mod function {
4755

4856
Ok(root)
4957
}
58+
59+
/// Prepare a `git` command, passing `root` as an operand to `-C`.
60+
///
61+
/// This is suitable when `git` gave us the path `root`. Then it should already be in a form
62+
/// where `git -C` will be able to use it, without alteration, regardless of the platform.
63+
/// (Otherwise, it may be preferable to set `root` as the `cwd` of the `git` process instead.)
64+
fn git_on(root: &OsStr) -> Command {
65+
let mut cmd = Command::new("git");
66+
cmd.arg("-C").arg(root);
67+
cmd
68+
}
69+
70+
static RECORD_REGEX: Lazy<Regex> = Lazy::new(|| {
71+
let pattern = r"(?-u)\A([0-7]+) ([[:xdigit:]]+) [[:digit:]]+\t(.+)\z";
72+
Regex::new(pattern).expect("regex should be valid")
73+
});
74+
75+
/// On mismatch, report it and return `Some(true)`.
76+
fn check_for_mismatch(root: &OsStr, record: &[u8]) -> anyhow::Result<bool> {
77+
let fields = RECORD_REGEX.captures(record).context("Malformed record from `git`")?;
78+
let mode = fields.get(1).expect("match should get mode").as_bytes();
79+
let oid = fields
80+
.get(2)
81+
.expect("match should get oid")
82+
.as_bytes()
83+
.to_os_str()
84+
.expect("oid field verified as hex digits, should be valid OsStr");
85+
let path = fields.get(3).expect("match should get path").as_bytes().as_bstr();
86+
87+
match mode {
88+
b"100644" if blob_has_shebang(root, oid)? => {
89+
println!("mode -x but has shebang: {}\n", path);
90+
Ok(true)
91+
}
92+
b"100755" if !blob_has_shebang(root, oid)? => {
93+
println!("mode +x but no shebang: {}\n", path);
94+
Ok(true)
95+
}
96+
_ => Ok(false),
97+
}
98+
}
99+
100+
fn blob_has_shebang(root: &OsStr, oid: &OsStr) -> anyhow::Result<bool> {
101+
let mut buf = [0u8; 2];
102+
103+
let mut child = git_on(root)
104+
.args(["cat-file", "blob"])
105+
.arg(oid)
106+
.stdout(Stdio::piped())
107+
.spawn()
108+
.context("Can't start `git` subprocess to read blob")?;
109+
110+
let mut stdout = child.stdout.take().expect("should have captured stdout");
111+
let count = stdout.read(&mut buf).context("Error reading data from blob")?;
112+
drop(stdout); // Let the pipe break rather than waiting for the rest of the blob.
113+
114+
// TODO: Maybe check status? On Unix, it should be 0 or SIGPIPE. Not sure about Windows.
115+
_ = child.wait().context("Failure running `git` subprocess to read blob")?;
116+
117+
let magic = &buf[..count];
118+
Ok(magic == b"#!")
119+
}
50120
}

0 commit comments

Comments
 (0)