fallow_engine/
changed_files.rs1use std::path::{Path, PathBuf};
4use std::process::Output;
5
6use fallow_types::results::AnalysisResults;
7use rustc_hash::FxHashSet;
8
9use crate::duplicates::DuplicationReport;
10
11pub type ChangedFilesSpawnHook = fn(&mut std::process::Command) -> std::io::Result<Output>;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ChangedFilesError {
18 InvalidRef(String),
20 GitMissing(String),
22 NotARepository,
24 GitFailed(String),
26}
27
28impl ChangedFilesError {
29 #[must_use]
31 pub fn describe(&self) -> String {
32 match self {
33 Self::InvalidRef(err) => format!("invalid git ref: {err}"),
34 Self::GitMissing(err) => format!("failed to run git: {err}"),
35 Self::NotARepository => "not a git repository".to_owned(),
36 Self::GitFailed(stderr) => augment_git_failed(stderr),
37 }
38 }
39}
40
41impl From<fallow_core::changed_files::ChangedFilesError> for ChangedFilesError {
42 fn from(error: fallow_core::changed_files::ChangedFilesError) -> Self {
43 match error {
44 fallow_core::changed_files::ChangedFilesError::InvalidRef(err) => Self::InvalidRef(err),
45 fallow_core::changed_files::ChangedFilesError::GitMissing(err) => Self::GitMissing(err),
46 fallow_core::changed_files::ChangedFilesError::NotARepository => Self::NotARepository,
47 fallow_core::changed_files::ChangedFilesError::GitFailed(stderr) => {
48 Self::GitFailed(stderr)
49 }
50 }
51 }
52}
53
54fn augment_git_failed(stderr: &str) -> String {
55 let lower = stderr.to_ascii_lowercase();
56 if lower.contains("not a valid object name")
57 || lower.contains("unknown revision")
58 || lower.contains("ambiguous argument")
59 {
60 format!(
61 "{stderr} (shallow clone? try `git fetch --unshallow`, or set `fetch-depth: 0` on actions/checkout / `GIT_DEPTH: 0` in GitLab CI)"
62 )
63 } else {
64 stderr.to_owned()
65 }
66}
67
68pub fn set_spawn_hook(hook: ChangedFilesSpawnHook) {
70 fallow_core::changed_files::set_spawn_hook(hook);
71}
72
73pub fn validate_git_ref(s: &str) -> Result<&str, String> {
75 fallow_core::changed_files::validate_git_ref(s)
76}
77
78pub fn resolve_git_toplevel(cwd: &Path) -> Result<PathBuf, ChangedFilesError> {
80 fallow_core::changed_files::resolve_git_toplevel(cwd).map_err(ChangedFilesError::from)
81}
82
83pub fn resolve_git_common_dir(cwd: &Path) -> Result<PathBuf, ChangedFilesError> {
85 fallow_core::changed_files::resolve_git_common_dir(cwd).map_err(ChangedFilesError::from)
86}
87
88pub fn try_get_changed_files(
90 root: &Path,
91 git_ref: &str,
92) -> Result<FxHashSet<PathBuf>, ChangedFilesError> {
93 fallow_core::changed_files::try_get_changed_files(root, git_ref)
94 .map_err(ChangedFilesError::from)
95}
96
97pub fn changed_files(root: &Path, git_ref: &str) -> Result<FxHashSet<PathBuf>, ChangedFilesError> {
103 try_get_changed_files(root, git_ref)
104}
105
106pub fn try_get_changed_files_with_toplevel(
108 cwd: &Path,
109 toplevel: &Path,
110 git_ref: &str,
111) -> Result<FxHashSet<PathBuf>, ChangedFilesError> {
112 fallow_core::changed_files::try_get_changed_files_with_toplevel(cwd, toplevel, git_ref)
113 .map_err(ChangedFilesError::from)
114}
115
116pub fn try_get_changed_diff(root: &Path, git_ref: &str) -> Result<String, ChangedFilesError> {
118 fallow_core::changed_files::try_get_changed_diff(root, git_ref).map_err(ChangedFilesError::from)
119}
120
121#[must_use]
123pub fn get_changed_files(root: &Path, git_ref: &str) -> Option<FxHashSet<PathBuf>> {
124 fallow_core::changed_files::get_changed_files(root, git_ref)
125}
126
127#[expect(
129 clippy::implicit_hasher,
130 reason = "fallow standardizes on FxHashSet across the workspace"
131)]
132pub fn filter_results_by_changed_files(
133 results: &mut AnalysisResults,
134 changed_files: &FxHashSet<PathBuf>,
135) {
136 fallow_core::changed_files::filter_results_by_changed_files(results, changed_files);
137}
138
139#[expect(
141 clippy::implicit_hasher,
142 reason = "fallow standardizes on FxHashSet across the workspace"
143)]
144pub fn filter_duplication_by_changed_files(
145 report: &mut DuplicationReport,
146 changed_files: &FxHashSet<PathBuf>,
147 root: &Path,
148) {
149 fallow_core::changed_files::filter_duplication_by_changed_files(report, changed_files, root);
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn changed_files_error_describe_matches_core_contract() {
158 assert_eq!(
159 ChangedFilesError::InvalidRef("bad ref".to_string()).describe(),
160 "invalid git ref: bad ref"
161 );
162 assert_eq!(
163 ChangedFilesError::GitMissing("not found".to_string()).describe(),
164 "failed to run git: not found"
165 );
166 assert_eq!(
167 ChangedFilesError::NotARepository.describe(),
168 "not a git repository"
169 );
170 assert!(
171 ChangedFilesError::GitFailed("unknown revision main".to_string())
172 .describe()
173 .contains("fetch-depth: 0")
174 );
175 }
176
177 #[test]
178 fn changed_files_error_converts_from_core_without_leaking_type() {
179 let error = fallow_core::changed_files::ChangedFilesError::GitFailed(
180 "ambiguous argument main".to_string(),
181 );
182 assert_eq!(
183 ChangedFilesError::from(error),
184 ChangedFilesError::GitFailed("ambiguous argument main".to_string())
185 );
186 }
187}