1#![expect(
4 clippy::disallowed_methods,
5 reason = "Running git as a subprocess is this crate's purpose; the invocations are confined to this boundary module."
6)]
7
8use std::path::Path;
9use std::process::Command;
10
11use crate::error::GitError;
12use crate::porcelain::parse_porcelain_v1z;
13use crate::status::{ChangeStatus, PorcelainOptions, WorktreeChange};
14
15pub fn worktree_changes(
21 repo_root: impl AsRef<Path>,
22 options: PorcelainOptions,
23) -> Result<Vec<WorktreeChange>, GitError> {
24 let repo_root = repo_root.as_ref();
25 let mut command = Command::new("git");
26 let _ = command.env("LC_ALL", "C");
28 let _ = command
29 .arg("-C")
30 .arg(repo_root)
31 .args(["status", "--porcelain=v1", "-z"]);
32 if options.include_ignored {
33 let _ = command.arg("--ignored");
34 }
35 let output = command.output().map_err(|source| {
36 if source.kind() == std::io::ErrorKind::NotFound {
37 GitError::GitNotInstalled
38 } else {
39 GitError::CommandFailed {
40 command: "git status --porcelain=v1 -z".to_owned(),
41 stderr: source.to_string(),
42 }
43 }
44 })?;
45 if !output.status.success() {
46 if !is_inside_work_tree(repo_root) {
48 return Err(GitError::NotARepository);
49 }
50 return Err(GitError::CommandFailed {
51 command: "git status --porcelain=v1 -z".to_owned(),
52 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
53 });
54 }
55 let text = String::from_utf8(output.stdout).map_err(|_| GitError::ParseError {
56 message: "porcelain output is not UTF-8".to_owned(),
57 })?;
58 let changes = parse_porcelain_v1z(&text)?;
59 Ok(changes
60 .into_iter()
61 .filter(|c| match c.status {
62 ChangeStatus::Untracked => options.include_untracked,
63 ChangeStatus::Ignored => options.include_ignored,
64 ChangeStatus::Tracked { .. } | ChangeStatus::Conflicted => true,
65 })
66 .collect())
67}
68
69fn is_inside_work_tree(repo_root: &Path) -> bool {
71 let mut command = Command::new("git");
72 let _ = command.env("LC_ALL", "C");
73 let output = command
74 .arg("-C")
75 .arg(repo_root)
76 .args(["rev-parse", "--is-inside-work-tree"])
77 .output();
78 output.is_ok_and(|out| out.status.success() && out.stdout.starts_with(b"true"))
79}