1use crate::bin::Opt;
2use anyhow::{bail, Context, Error};
3use std::process::{Command, Output};
4use std::str;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
12enum GitError {
13 #[error("failed to execute git command ({})\n{}", cmd, err)]
14 ExecFailed { cmd: String, err: String },
15
16 #[error("failed to call git command ({})", cmd)]
17 CallFailed { cmd: String },
18
19 #[error("failed to convert to UTF-8 ({:?})", s)]
20 ConvFailed { s: Vec<u8> },
21}
22
23pub struct CmdGit;
28
29impl CmdGit {
30 pub fn get_files(opt: &Opt) -> Result<Vec<String>, Error> {
31 let mut list = CmdGit::ls_files(&opt)?;
32 if opt.exclude_lfs {
33 let lfs_list = CmdGit::lfs_ls_files(&opt)?;
34 let mut new_list = Vec::new();
35 for l in list {
36 if !lfs_list.contains(&l) {
37 new_list.push(l);
38 }
39 }
40 list = new_list;
41 }
42 Ok(list)
43 }
44
45 fn call(opt: &Opt, args: &[String]) -> Result<Output, Error> {
46 let cmd = CmdGit::get_cmd(&opt, &args);
47 if opt.verbose {
48 eprintln!("Call : {}", cmd);
49 }
50
51 let output = Command::new(&opt.bin_git)
52 .args(args)
53 .current_dir(&opt.dir)
54 .output()
55 .context(GitError::CallFailed { cmd: cmd.clone() })?;
56
57 if !output.status.success() {
58 bail!(GitError::ExecFailed {
59 cmd: cmd,
60 err: String::from(str::from_utf8(&output.stderr).context(
61 GitError::ConvFailed {
62 s: output.stderr.to_vec(),
63 }
64 )?)
65 });
66 }
67
68 Ok(output)
69 }
70
71 fn ls_files(opt: &Opt) -> Result<Vec<String>, Error> {
72 let mut args = vec![String::from("ls-files")];
73 args.push(String::from("--cached"));
74 args.push(String::from("--exclude-standard"));
75 if opt.include_submodule {
76 args.push(String::from("--recurse-submodules"));
77 } else if opt.include_untracked {
78 args.push(String::from("--other"));
79 } else if opt.include_ignored {
80 args.push(String::from("--ignored"));
81 args.push(String::from("--other"));
82 }
83 args.append(&mut opt.opt_git.clone());
84
85 let output = CmdGit::call(&opt, &args)?;
86
87 let list = str::from_utf8(&output.stdout)
88 .context(GitError::ConvFailed {
89 s: output.stdout.to_vec(),
90 })?
91 .lines();
92 let mut ret = Vec::new();
93 for l in list {
94 ret.push(String::from(l));
95 }
96 ret.sort();
97
98 if opt.verbose {
99 eprintln!("Files: {}", ret.len());
100 }
101
102 Ok(ret)
103 }
104
105 fn lfs_ls_files(opt: &Opt) -> Result<Vec<String>, Error> {
106 let mut args = vec![String::from("lfs"), String::from("ls-files")];
107 args.append(&mut opt.opt_git_lfs.clone());
108
109 let output = CmdGit::call(&opt, &args)?;
110
111 let cdup = CmdGit::show_cdup(&opt)?;
112 let prefix = CmdGit::show_prefix(&opt)?;
113
114 let list = str::from_utf8(&output.stdout)
115 .context(GitError::ConvFailed {
116 s: output.stdout.to_vec(),
117 })?
118 .lines();
119 let mut ret = Vec::new();
120 for l in list {
121 let mut path = String::from(l.split(' ').nth(2).unwrap_or(""));
122 if path.starts_with(&prefix) {
123 path = path.replace(&prefix, "");
124 } else {
125 path = format!("{}{}", cdup, path);
126 }
127 ret.push(path);
128 }
129 ret.sort();
130 Ok(ret)
131 }
132
133 fn show_cdup(opt: &Opt) -> Result<String, Error> {
134 let args = vec![String::from("rev-parse"), String::from("--show-cdup")];
135
136 let output = CmdGit::call(&opt, &args)?;
137
138 let mut list = str::from_utf8(&output.stdout)
139 .context(GitError::ConvFailed {
140 s: output.stdout.to_vec(),
141 })?
142 .lines();
143 Ok(String::from(list.next().unwrap_or("")))
144 }
145
146 fn show_prefix(opt: &Opt) -> Result<String, Error> {
147 let args = vec![String::from("rev-parse"), String::from("--show-prefix")];
148
149 let output = CmdGit::call(&opt, &args)?;
150
151 let mut list = str::from_utf8(&output.stdout)
152 .context(GitError::ConvFailed {
153 s: output.stdout.to_vec(),
154 })?
155 .lines();
156 Ok(String::from(list.next().unwrap_or("")))
157 }
158
159 fn get_cmd(opt: &Opt, args: &[String]) -> String {
160 let mut cmd = format!(
161 "cd {}; {}",
162 opt.dir.to_string_lossy(),
163 opt.bin_git.to_string_lossy()
164 );
165 for arg in args {
166 cmd = format!("{} {}", cmd, arg);
167 }
168 cmd
169 }
170}
171
172#[cfg(test)]
177mod tests {
178 use super::CmdGit;
179 use crate::bin::Opt;
180 use std::fs;
181 use std::io::{BufWriter, Write};
182 use structopt::StructOpt;
183
184 static TRACKED_FILES: [&'static str; 23] = [
185 ".cargo/config",
186 ".gitattributes",
187 ".github/FUNDING.yml",
188 ".github/dependabot.yml",
189 ".github/workflows/dependabot_merge.yml",
190 ".github/workflows/periodic.yml",
191 ".github/workflows/regression.yml",
192 ".github/workflows/release.yml",
193 ".gitignore",
194 ".gitmodules",
195 "Cargo.lock",
196 "Cargo.toml",
197 "LICENSE",
198 "Makefile",
199 "README.md",
200 "benches/ptags_bench.rs",
201 "src/bin.rs",
202 "src/cmd_ctags.rs",
203 "src/cmd_git.rs",
204 "src/lib.rs",
205 "src/main.rs",
206 "test/lfs.txt",
207 "test/ptags_test",
208 ];
209
210 #[test]
211 fn test_get_files() {
212 let args = vec!["ptags"];
213 let opt = Opt::from_iter(args.iter());
214 let files = CmdGit::get_files(&opt).unwrap();
215 assert_eq!(files, TRACKED_FILES,);
216 }
217
218 #[test]
219 fn test_get_files_exclude_lfs() {
220 let args = vec!["ptags", "--exclude-lfs"];
221 let opt = Opt::from_iter(args.iter());
222 let files = CmdGit::get_files(&opt).unwrap();
223
224 let mut expect_files = Vec::new();
225 expect_files.extend_from_slice(&TRACKED_FILES);
226 let idx = expect_files.binary_search(&"test/lfs.txt").unwrap();
227 expect_files.remove(idx);
228
229 assert_eq!(files, expect_files,);
230 }
231
232 #[test]
233 fn test_get_files_exclude_lfs_cd() {
234 let args = vec!["ptags", "--exclude-lfs", "src"];
235 let opt = Opt::from_iter(args.iter());
236 let files = CmdGit::get_files(&opt).unwrap();
237 assert_eq!(
238 files,
239 vec!["bin.rs", "cmd_ctags.rs", "cmd_git.rs", "lib.rs", "main.rs"]
240 );
241 }
242
243 #[test]
244 fn test_get_files_include_ignored() {
245 {
246 let mut f = BufWriter::new(fs::File::create("ignored.gz").unwrap());
247 let _ = f.write(b"");
248 }
249 let args = vec!["ptags", "--include-ignored"];
250 let opt = Opt::from_iter(args.iter());
251 let files: Vec<String> = CmdGit::get_files(&opt)
252 .unwrap()
253 .into_iter()
254 .filter(|f| !f.starts_with("target/"))
255 .collect();
256 let _ = fs::remove_file("ignored.gz");
257
258 let mut expect_files = Vec::new();
259 expect_files.push("ignored.gz");
260 expect_files.push("tags");
261
262 assert_eq!(files, expect_files,);
263 }
264
265 #[test]
266 fn test_get_files_include_submodule() {
267 let args = vec!["ptags", "--include-submodule"];
268 let opt = Opt::from_iter(args.iter());
269 let files = CmdGit::get_files(&opt).unwrap();
270
271 let mut expect_files = Vec::new();
272 expect_files.extend_from_slice(&TRACKED_FILES);
273 let idx = expect_files.binary_search(&"test/ptags_test").unwrap();
274 expect_files.remove(idx);
275 expect_files.push("test/ptags_test/README.md");
276
277 assert_eq!(files, expect_files,);
278 }
279
280 #[test]
281 fn test_get_files_include_untracked() {
282 {
283 let mut f = BufWriter::new(fs::File::create("tmp").unwrap());
284 let _ = f.write(b"");
285 }
286 let args = vec!["ptags", "--include-untracked"];
287 let opt = Opt::from_iter(args.iter());
288 let files = CmdGit::get_files(&opt).unwrap();
289 let _ = fs::remove_file("tmp");
290
291 let mut expect_files = Vec::new();
292 expect_files.extend_from_slice(&TRACKED_FILES);
293 expect_files.push("tmp");
294
295 assert_eq!(files, expect_files,);
296 }
297
298 #[test]
299 fn test_command_fail() {
300 let args = vec!["ptags", "--bin-git", "aaa"];
301 let opt = Opt::from_iter(args.iter());
302 let files = CmdGit::ls_files(&opt);
303 assert_eq!(
304 &format!("{:?}", files)[0..42],
305 "Err(failed to call git command (cd .; aaa "
306 );
307 }
308
309 #[test]
310 fn test_git_fail() {
311 let args = vec!["ptags", "--opt-git=-aaa"];
312 let opt = Opt::from_iter(args.iter());
313 let files = CmdGit::ls_files(&opt);
314 assert_eq!(
315 &format!("{:?}", files)[0..83],
316 "Err(failed to execute git command (cd .; git ls-files --cached --exclude-standard -"
317 );
318 }
319}