1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//! Git operations
use std::path::{Path, PathBuf};

use xvc_walker::{build_ignore_rules, AbsolutePath, IgnoreRules};

use crate::error::Result;
use crate::GIT_DIR;

/// Check whether a path is inside a Git repository.
/// It returns `None` if not, otherwise returns the closest directory with `.git`.
/// It works by checking `.git` directories in parents, until no more parent left.
pub fn inside_git(path: &Path) -> Option<PathBuf> {
    let mut pb = PathBuf::from(path)
        .canonicalize()
        .expect("Cannot canonicalize the path. Possible symlink loop.");
    loop {
        if pb.join(GIT_DIR).is_dir() {
            return Some(pb);
        } else if pb.parent() == None {
            return None;
        } else {
            pb.pop();
        }
    }
}

/// Returns [xvc_walker::IgnoreRules] for `.gitignore`
/// It's used to check whether a path is already ignored by Git.
pub fn build_gitignore(git_root: &AbsolutePath) -> Result<IgnoreRules> {
    let initial_rules = IgnoreRules::empty(git_root);

    let rules = build_ignore_rules(initial_rules, git_root, ".gitignore")?;

    Ok(rules)
}

#[cfg(test)]
mod test {
    use super::*;
    use std::fs;
    use test_case::test_case;
    use xvc_logging::watch;
    use xvc_test_helper::*;
    use xvc_walker::check_ignore;
    use xvc_walker::MatchResult as M;

    #[test_case("myfile.txt" , ".gitignore", "/myfile.txt" => matches M::Ignore ; "myfile.txt")]
    #[test_case("mydir/myfile.txt" , "mydir/.gitignore", "myfile.txt" => matches M::Ignore ; "mydir/myfile.txt")]
    #[test_case("mydir/myfile.txt" , ".gitignore", "/mydir/myfile.txt" => matches M::Ignore ; "from root dir")]
    #[test_case("mydir/myfile.txt" , ".gitignore", ""  => matches M::NoMatch ; "non ignore")]
    #[test_case("mydir/myfile.txt" , ".gitignore", "mydir/**" => matches M::Ignore ; "ignore dir star 2")]
    #[test_case("mydir/myfile.txt" , ".gitignore", "mydir/*" => matches M::Ignore ; "ignore dir star")]
    #[test_case("mydir/yourdir/myfile.txt" , "mydir/.gitignore", "yourdir/**" => matches M::Ignore ; "ignore deep dir star 2")]
    #[test_case("mydir/yourdir/myfile.txt" , "mydir/.gitignore", "yourdir/*" => matches M::Ignore ; "ignore deep dir star")]
    #[test_case("mydir/myfile.txt" , "hebelep/.gitignore", "hebelep/myfile.txt" => matches M::NoMatch ; "non ignore from dir")]
    fn test_gitignore(path: &str, gitignore_path: &str, ignore_line: &str) -> M {
        test_logging(log::LevelFilter::Trace);
        let git_root = temp_git_dir();
        let path = git_root.join(PathBuf::from(path));
        let gitignore_path = git_root.join(PathBuf::from(gitignore_path));
        if let Some(ignore_dir) = gitignore_path.parent() {
            fs::create_dir_all(&ignore_dir).unwrap();
        }
        fs::write(&gitignore_path, format!("{}\n", ignore_line)).unwrap();

        let gitignore = build_ignore_rules(
            IgnoreRules::empty(&git_root),
            &gitignore_path.parent().unwrap(),
            ".gitignore",
        )
        .unwrap();

        watch!(gitignore);

        let matches = check_ignore(&gitignore, &path);

        matches
    }
}