Skip to main content

vcs_runner/
worktree.rs

1//! Working-tree file access.
2//!
3//! The working copy lives on disk, so its content is read directly here rather
4//! than through jj or git. A direct disk read is the working-copy-agnostic way
5//! to get the "working" (current) side of a diff: it never snapshots the jj
6//! working copy, so a long-lived reader — a TUI refreshing on every save —
7//! doesn't churn the operation log or race the user's foreground jj commands.
8//! Pairs with [`crate::run_jj_utf8_ignore_wc`], which covers the committed/repo
9//! side without snapshotting; between them a consumer can show a live diff
10//! (committed base vs on-disk working copy) while writing nothing.
11//!
12//! `rel_path` is always relative to `repo_path`. An absent file yields
13//! `Ok(None)` (it reads as deleted), so callers don't special-case existence.
14
15use std::path::Path;
16
17/// Read a working-tree file's current on-disk content as text (lossy UTF-8).
18/// `Ok(None)` when the file is absent (e.g. deleted); `Err` only on a real I/O
19/// error.
20pub fn read_working_file(repo_path: &Path, rel_path: &str) -> std::io::Result<Option<String>> {
21    match std::fs::read(repo_path.join(rel_path)) {
22        Ok(bytes) => Ok(Some(String::from_utf8_lossy(&bytes).into_owned())),
23        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
24        Err(e) => Err(e),
25    }
26}
27
28/// Bytes variant of [`read_working_file`], for image/binary handling.
29pub fn read_working_file_bytes(
30    repo_path: &Path,
31    rel_path: &str,
32) -> std::io::Result<Option<Vec<u8>>> {
33    match std::fs::read(repo_path.join(rel_path)) {
34        Ok(bytes) => Ok(Some(bytes)),
35        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
36        Err(e) => Err(e),
37    }
38}
39
40/// Whether a working-tree file looks binary — a NUL byte in the first 8 KiB
41/// (git's heuristic). Reads only that prefix, so a large binary isn't slurped in
42/// full just to be classified. Absent or unreadable ⇒ `false`.
43pub fn working_file_is_binary(repo_path: &Path, rel_path: &str) -> bool {
44    use std::io::Read;
45    let Ok(f) = std::fs::File::open(repo_path.join(rel_path)) else {
46        return false;
47    };
48    // Read up to 8 KiB reliably — a single `read` may short-read, so `take` +
49    // `read_to_end` (which loops to the limit or EOF) is used rather than one
50    // `read` into a fixed buffer.
51    let mut buf = Vec::with_capacity(8192);
52    if f.take(8192).read_to_end(&mut buf).is_err() {
53        return false;
54    }
55    buf.contains(&0)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn reads_present_file_and_none_for_absent() {
64        let dir = tempfile::tempdir().unwrap();
65        std::fs::write(dir.path().join("a.txt"), "hello\n").unwrap();
66        assert_eq!(
67            read_working_file(dir.path(), "a.txt").unwrap().as_deref(),
68            Some("hello\n")
69        );
70        assert_eq!(read_working_file(dir.path(), "missing.txt").unwrap(), None);
71    }
72
73    #[test]
74    fn bytes_variant_round_trips_and_none_for_absent() {
75        let dir = tempfile::tempdir().unwrap();
76        std::fs::write(dir.path().join("b.bin"), [0u8, 1, 2, 255]).unwrap();
77        assert_eq!(
78            read_working_file_bytes(dir.path(), "b.bin").unwrap(),
79            Some(vec![0, 1, 2, 255])
80        );
81        assert_eq!(read_working_file_bytes(dir.path(), "nope").unwrap(), None);
82    }
83
84    #[test]
85    fn binary_detection_flags_nul_and_clears_text() {
86        let dir = tempfile::tempdir().unwrap();
87        std::fs::write(dir.path().join("t.txt"), "plain text\n").unwrap();
88        std::fs::write(dir.path().join("b.bin"), [b'x', 0, b'y']).unwrap();
89        assert!(!working_file_is_binary(dir.path(), "t.txt"));
90        assert!(working_file_is_binary(dir.path(), "b.bin"));
91        assert!(!working_file_is_binary(dir.path(), "absent"));
92    }
93
94    #[test]
95    fn binary_detection_scans_the_full_8kib_window() {
96        let dir = tempfile::tempdir().unwrap();
97        // NUL near the end of the window must be found — guards against a
98        // short-read only inspecting the first chunk.
99        let mut data = vec![b'a'; 8000];
100        data.push(0);
101        std::fs::write(dir.path().join("mid.bin"), &data).unwrap();
102        assert!(working_file_is_binary(dir.path(), "mid.bin"));
103    }
104
105    #[test]
106    fn binary_detection_stops_at_8kib() {
107        let dir = tempfile::tempdir().unwrap();
108        // A NUL just past the 8 KiB window is not scanned (git's heuristic).
109        let mut data = vec![b'a'; 8192];
110        data.push(0);
111        std::fs::write(dir.path().join("late.bin"), &data).unwrap();
112        assert!(!working_file_is_binary(dir.path(), "late.bin"));
113    }
114}