|
| 1 | +pub(super) mod function { |
| 2 | + use anyhow::{bail, Context}; |
| 3 | + use gix::bstr::ByteSlice; |
| 4 | + use once_cell::sync::Lazy; |
| 5 | + use regex::bytes::Regex; |
| 6 | + use std::ffi::{OsStr, OsString}; |
| 7 | + use std::io::{BufRead, BufReader, Read}; |
| 8 | + use std::process::{Command, Stdio}; |
| 9 | + |
| 10 | + pub fn check_mode() -> anyhow::Result<()> { |
| 11 | + let root = find_root()?; |
| 12 | + let mut any_mismatch = false; |
| 13 | + |
| 14 | + let mut child = git_on(&root) |
| 15 | + .args(["ls-files", "-sz", "--", "*.sh"]) |
| 16 | + .stdout(Stdio::piped()) |
| 17 | + .spawn() |
| 18 | + .context("Can't start `git` subprocess to list index")?; |
| 19 | + |
| 20 | + let stdout = child.stdout.take().expect("should have captured stdout"); |
| 21 | + for result in BufReader::new(stdout).split(0) { |
| 22 | + let record = result.context(r"Can't read '\0'-terminated record")?; |
| 23 | + any_mismatch |= check_for_mismatch(&root, &record)?; |
| 24 | + } |
| 25 | + |
| 26 | + let status = child.wait().context("Failure running `git` subprocess to list index")?; |
| 27 | + if !status.success() { |
| 28 | + bail!("`git` subprocess to list index did not complete successfully"); |
| 29 | + } |
| 30 | + if any_mismatch { |
| 31 | + bail!("Mismatch found - scan completed, finding at least one `#!` vs. `+x` mismatch"); |
| 32 | + } |
| 33 | + Ok(()) |
| 34 | + } |
| 35 | + |
| 36 | + /// Find the top-level directory of the current repository working tree. |
| 37 | + fn find_root() -> anyhow::Result<OsString> { |
| 38 | + let output = Command::new(gix::path::env::exe_invocation()) |
| 39 | + .args(["rev-parse", "--show-toplevel"]) |
| 40 | + .output() |
| 41 | + .context("Can't run `git` to find worktree root")?; |
| 42 | + |
| 43 | + if !output.status.success() { |
| 44 | + bail!("`git` failed to find worktree root"); |
| 45 | + } |
| 46 | + |
| 47 | + let root = output |
| 48 | + .stdout |
| 49 | + .strip_suffix(b"\n") |
| 50 | + .context("Can't parse worktree root")? |
| 51 | + .to_os_str()? |
| 52 | + .to_owned(); |
| 53 | + Ok(root) |
| 54 | + } |
| 55 | + |
| 56 | + /// Prepare a `git` command, passing `root` as an operand to `-C`. |
| 57 | + /// |
| 58 | + /// This is suitable when `git` gave us the path `root`. Then it should already be in a form |
| 59 | + /// where `git -C` will be able to use it, without alteration, regardless of the platform. |
| 60 | + /// (Otherwise, it may be preferable to set `root` as the `cwd` of the `git` process instead.) |
| 61 | + fn git_on(root: &OsStr) -> Command { |
| 62 | + let mut cmd = Command::new(gix::path::env::exe_invocation()); |
| 63 | + cmd.arg("-C").arg(root); |
| 64 | + cmd |
| 65 | + } |
| 66 | + |
| 67 | + /// On mismatch, report it and return `Some(true)`. |
| 68 | + fn check_for_mismatch(root: &OsStr, record: &[u8]) -> anyhow::Result<bool> { |
| 69 | + static RECORD_REGEX: Lazy<Regex> = Lazy::new(|| { |
| 70 | + let pattern = r"(?-u)\A([0-7]+) ([[:xdigit:]]+) [[:digit:]]+\t(.+)\z"; |
| 71 | + Regex::new(pattern).expect("regex should be valid") |
| 72 | + }); |
| 73 | + |
| 74 | + let fields = RECORD_REGEX.captures(record).context("Malformed record from `git`")?; |
| 75 | + let mode = fields.get(1).expect("match should get mode").as_bytes(); |
| 76 | + let oid = fields |
| 77 | + .get(2) |
| 78 | + .expect("match should get oid") |
| 79 | + .as_bytes() |
| 80 | + .to_os_str() |
| 81 | + .expect("oid field verified as hex digits, should be valid OsStr"); |
| 82 | + let path = fields.get(3).expect("match should get path").as_bytes().as_bstr(); |
| 83 | + |
| 84 | + match mode { |
| 85 | + b"100644" if blob_has_shebang(root, oid)? => { |
| 86 | + println!("mode -x but has shebang: {path:?}"); |
| 87 | + Ok(true) |
| 88 | + } |
| 89 | + b"100755" if !blob_has_shebang(root, oid)? => { |
| 90 | + println!("mode +x but no shebang: {path:?}"); |
| 91 | + Ok(true) |
| 92 | + } |
| 93 | + _ => Ok(false), |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + fn blob_has_shebang(root: &OsStr, oid: &OsStr) -> anyhow::Result<bool> { |
| 98 | + let mut buf = [0u8; 2]; |
| 99 | + |
| 100 | + let mut child = git_on(root) |
| 101 | + .args(["cat-file", "blob"]) |
| 102 | + .arg(oid) |
| 103 | + .stdout(Stdio::piped()) |
| 104 | + .spawn() |
| 105 | + .context("Can't start `git` subprocess to read blob")?; |
| 106 | + |
| 107 | + let mut stdout = child.stdout.take().expect("should have captured stdout"); |
| 108 | + let count = stdout.read(&mut buf).context("Error reading data from blob")?; |
| 109 | + drop(stdout); // Let the pipe break rather than waiting for the rest of the blob. |
| 110 | + |
| 111 | + // TODO: Maybe check status? On Unix, it should be 0 or SIGPIPE. Not sure about Windows. |
| 112 | + _ = child.wait().context("Failure running `git` subprocess to read blob")?; |
| 113 | + |
| 114 | + Ok(&buf[..count] == b"#!") |
| 115 | + } |
| 116 | +} |
0 commit comments