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