tytanic_core/project/
vcs.rs1use std::fmt::{self, Debug, Display};
9use std::path::{Path, PathBuf};
10use std::{fs, io};
11
12use crate::test::Test;
13
14use super::Project;
15
16const GITIGNORE_NAME: &str = ".gitignore";
18
19const HGIGNORE_NAME: &str = ".hgignore";
21
22const IGNORE_HEADER: &str = "# generated by tytanic, do not edit";
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum Kind {
28 Git,
33
34 Mercurial,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct Vcs {
45 root: PathBuf,
46 kind: Kind,
47}
48
49impl Vcs {
50 pub fn new<I>(root: I, kind: Kind) -> Self
52 where
53 I: Into<PathBuf>,
54 {
55 Self {
56 root: root.into(),
57 kind,
58 }
59 }
60
61 pub fn exists_at(dir: &Path) -> io::Result<Option<Kind>> {
64 if dir.join(".git").try_exists()? || dir.join(".jj").try_exists()? {
65 return Ok(Some(Kind::Git));
66 }
67
68 if dir.join(".hg").try_exists()? {
69 return Ok(Some(Kind::Mercurial));
70 }
71
72 Ok(None)
73 }
74}
75
76impl Vcs {
77 pub fn root(&self) -> &Path {
79 &self.root
80 }
81
82 pub fn kind(&self) -> Kind {
84 self.kind
85 }
86
87 pub fn ignore(&self, project: &Project, test: &Test) -> io::Result<()> {
89 let mut content = format!("{IGNORE_HEADER}\n\n");
90
91 let file = project.unit_test_dir(test.id()).join(match self.kind {
92 Kind::Git => GITIGNORE_NAME,
93 Kind::Mercurial => {
94 content.push_str("syntax: glob\n");
95 HGIGNORE_NAME
96 }
97 });
98
99 for always in ["diff/**\n", "out/**\n"] {
100 content.push_str(always);
101 }
102
103 if !test.kind().is_persistent() {
104 content.push_str("ref/**\n");
105 }
106
107 fs::write(file, content)?;
108
109 Ok(())
110 }
111
112 pub fn unignore(&self, project: &Project, test: &Test) -> io::Result<()> {
113 let file = project.unit_test_dir(test.id()).join(match self.kind {
114 Kind::Git => GITIGNORE_NAME,
115 Kind::Mercurial => HGIGNORE_NAME,
116 });
117
118 fs::remove_file(file)
119 }
120}
121
122impl Display for Vcs {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.pad(match self.kind {
125 Kind::Git => "Git",
126 Kind::Mercurial => "Mercurial",
127 })
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use tytanic_utils::fs::TempTestEnv;
134
135 use super::*;
136 use crate::test::{Id, Kind as TestKind};
137
138 fn test(kind: TestKind) -> Test {
139 Test::new_test(Id::new("fancy").unwrap(), kind)
140 }
141
142 #[test]
143 fn test_git_ignore_create() {
144 TempTestEnv::run(
145 |root| root.setup_dir("tests/fancy"),
146 |root| {
147 let project = Project::new(root);
148 let vcs = Vcs::new(root, Kind::Git);
149 let test = test(TestKind::CompileOnly);
150 vcs.ignore(&project, &test).unwrap();
151 },
152 |root| {
153 root.expect_dir("tests/fancy").expect_file_content(
154 "tests/fancy/.gitignore",
155 format!("{IGNORE_HEADER}\n\ndiff/**\nout/**\nref/**\n"),
156 )
157 },
158 );
159 }
160
161 #[test]
162 fn test_git_ignore_truncate() {
163 TempTestEnv::run(
164 |root| root.setup_file("tests/fancy/.gitignore", "blah blah"),
165 |root| {
166 let project = Project::new(root);
167 let vcs = Vcs::new(root, Kind::Git);
168 let test = test(TestKind::CompileOnly);
169 vcs.ignore(&project, &test).unwrap();
170 },
171 |root| {
172 root.expect_dir("tests/fancy").expect_file_content(
173 "tests/fancy/.gitignore",
174 format!("{IGNORE_HEADER}\n\ndiff/**\nout/**\nref/**\n"),
175 )
176 },
177 );
178 }
179
180 #[test]
181 fn test_git_unignore() {
182 TempTestEnv::run(
183 |root| root.setup_file("tests/fancy/.gitignore", "blah blah"),
184 |root| {
185 let project = Project::new(root);
186 let vcs = Vcs::new(root, Kind::Git);
187 let test = test(TestKind::CompileOnly);
188 vcs.unignore(&project, &test).unwrap();
189 },
190 |root| root.expect_dir("tests/fancy"),
191 );
192 }
193}