ptagslib/
cmd_git.rs

1use crate::bin::Opt;
2use anyhow::{bail, Context, Error};
3use std::process::{Command, Output};
4use std::str;
5use thiserror::Error;
6
7// ---------------------------------------------------------------------------------------------------------------------
8// Error
9// ---------------------------------------------------------------------------------------------------------------------
10
11#[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
23// ---------------------------------------------------------------------------------------------------------------------
24// CmdGit
25// ---------------------------------------------------------------------------------------------------------------------
26
27pub 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// ---------------------------------------------------------------------------------------------------------------------
173// Test
174// ---------------------------------------------------------------------------------------------------------------------
175
176#[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}