1use std::env::current_dir;
2use std::path::PathBuf;
3
4use pijul_config as config;
5
6use anyhow::bail;
7use libpijul::DOT_DIR;
8use log::debug;
9
10pub struct Repository {
11 pub pristine: libpijul::pristine::sanakirja::Pristine,
12 pub changes: libpijul::changestore::filesystem::FileSystem,
13 pub working_copy: libpijul::working_copy::filesystem::FileSystem,
14 pub config: config::Config,
15 pub path: PathBuf,
16 pub changes_dir: PathBuf,
17}
18
19pub const PRISTINE_DIR: &str = "pristine";
20pub const CHANGES_DIR: &str = "changes";
21pub const CONFIG_FILE: &str = "config";
22const DEFAULT_IGNORE: [&[u8]; 2] = [b".git", b".DS_Store"];
23const IGNORE_KINDS: &[(&[&str], &[&[u8]])] = &[
26 (&["rust"], &[b"/target", b"Cargo.lock"]),
27 (&["node", "nodejs"], &[b"node_modules"]),
28 (&["lean"], &[b"/build"]),
29];
30
31#[cfg(unix)]
32pub fn max_files() -> std::io::Result<usize> {
33 let n = if let Ok((n, _)) = rlimit::getrlimit(rlimit::Resource::NOFILE) {
34 (n as usize / (2 * std::thread::available_parallelism()?.get())).max(1)
35 } else {
36 256
37 };
38 debug!("max_files = {:?}", n);
39 Ok(n)
40}
41
42#[cfg(not(unix))]
43pub fn max_files() -> std::io::Result<usize> {
44 Ok(1)
45}
46
47impl Repository {
48 fn find_root_(cur: Option<PathBuf>, dot_dir: &str) -> Result<PathBuf, anyhow::Error> {
49 let mut cur = if let Some(cur) = cur {
50 cur
51 } else {
52 current_dir()?
53 };
54 cur.push(dot_dir);
55 loop {
56 debug!("{:?}", cur);
57 if std::fs::metadata(&cur).is_err() {
58 cur.pop();
59 if cur.pop() {
60 cur.push(DOT_DIR);
61 } else {
62 bail!("No Pijul repository found")
63 }
64 } else {
65 break;
66 }
67 }
68 Ok(cur)
69 }
70
71 pub fn find_root(cur: Option<PathBuf>) -> Result<Self, anyhow::Error> {
72 Self::find_root_with_dot_dir(cur, DOT_DIR)
73 }
74
75 pub fn find_root_with_dot_dir(
76 cur: Option<PathBuf>,
77 dot_dir: &str,
78 ) -> Result<Self, anyhow::Error> {
79 let cur = Self::find_root_(cur, dot_dir)?;
80 let mut pristine_dir = cur.clone();
81 pristine_dir.push(PRISTINE_DIR);
82 let mut changes_dir = cur.clone();
83 changes_dir.push(CHANGES_DIR);
84 let mut working_copy_dir = cur.clone();
85 working_copy_dir.pop();
86 let config_path = cur.join(CONFIG_FILE);
87 let config = if let Ok(config) = std::fs::read(&config_path) {
88 if let Ok(toml) = toml::from_str(&String::from_utf8(config)?) {
89 toml
90 } else {
91 bail!("Could not read configuration file at {:?}", config_path)
92 }
93 } else {
94 config::Config::default()
95 };
96 Ok(Repository {
97 pristine: libpijul::pristine::sanakirja::Pristine::new(&pristine_dir.join("db"))?,
98 working_copy: libpijul::working_copy::filesystem::FileSystem::from_root(
99 &working_copy_dir,
100 ),
101 changes: libpijul::changestore::filesystem::FileSystem::from_root(
102 &working_copy_dir,
103 max_files()?,
104 ),
105 config,
106 path: working_copy_dir,
107 changes_dir,
108 })
109 }
110
111 pub fn init(
112 path: Option<std::path::PathBuf>,
113 kind: Option<&str>,
114 remote: Option<&str>,
115 ) -> Result<Self, anyhow::Error> {
116 use std::io::Write;
117
118 let cur = if let Some(path) = path {
119 path
120 } else {
121 current_dir()?
122 };
123 let pristine_dir = {
124 let mut base = cur.clone();
125 base.push(DOT_DIR);
126 base.push(PRISTINE_DIR);
127 base
128 };
129 if std::fs::metadata(&pristine_dir).is_err() {
130 std::fs::create_dir_all(&pristine_dir)?;
131 init_dot_ignore(cur.clone(), kind)?;
132 init_default_config(&cur, remote)?;
133 let changes_dir = {
134 let mut base = cur.clone();
135 base.push(DOT_DIR);
136 base.push(CHANGES_DIR);
137 base
138 };
139
140 let mut stderr = std::io::stderr();
141 writeln!(stderr, "Repository created at {}", cur.to_string_lossy())?;
142
143 Ok(Repository {
144 pristine: libpijul::pristine::sanakirja::Pristine::new(&pristine_dir.join("db"))?,
145 working_copy: libpijul::working_copy::filesystem::FileSystem::from_root(&cur),
146 changes: libpijul::changestore::filesystem::FileSystem::from_root(
147 &cur,
148 max_files()?,
149 ),
150 config: config::Config::default(),
151 path: cur,
152 changes_dir,
153 })
154 } else {
155 bail!("Already in a repository")
156 }
157 }
158
159 pub fn update_config(&self) -> Result<(), anyhow::Error> {
160 std::fs::write(
161 self.path.join(DOT_DIR).join("config"),
162 toml::to_string(&self.config)?,
163 )?;
164 Ok(())
165 }
166}
167
168fn init_default_config(path: &std::path::Path, remote: Option<&str>) -> Result<(), anyhow::Error> {
169 use std::io::Write;
170 let mut path = path.join(DOT_DIR);
171 path.push("config");
172 if std::fs::metadata(&path).is_err() {
173 let mut f = std::fs::File::create(&path)?;
174 if let Some(rem) = remote {
175 writeln!(f, "default_remote = {:?}", rem)?;
176 }
177 writeln!(f, "[hooks]\nrecord = []")?;
178 }
179 Ok(())
180}
181
182fn init_dot_ignore(base_path: std::path::PathBuf, kind: Option<&str>) -> Result<(), anyhow::Error> {
185 use std::io::Write;
186 let dot_ignore_path = {
187 let mut base = base_path.clone();
188 base.push(".ignore");
189 base
190 };
191
192 if dot_ignore_path.exists() {
194 Ok(())
195 } else {
196 let mut dot_ignore = std::fs::OpenOptions::new()
197 .read(true)
198 .write(true)
199 .create(true)
200 .open(dot_ignore_path)?;
201
202 for default_ignore in DEFAULT_IGNORE.iter() {
203 dot_ignore.write_all(default_ignore)?;
204 dot_ignore.write_all(b"\n")?;
205 }
206 ignore_specific(&mut dot_ignore, kind)
207 }
208}
209
210fn ignore_specific(
213 dot_ignore: &mut std::fs::File,
214 kind: Option<&str>,
215) -> Result<(), anyhow::Error> {
216 use std::io::Write;
217 if let Some(kind) = kind {
218 if let Ok((config, _)) = pijul_config::Global::load() {
219 let ignore_kinds = config.ignore_kinds.as_ref();
220 if let Some(kinds) = ignore_kinds.and_then(|x| x.get(kind)) {
221 for entry in kinds.iter() {
222 writeln!(dot_ignore, "{}", entry)?;
223 }
224 return Ok(());
225 }
226 }
227 let entries = IGNORE_KINDS
228 .iter()
229 .find(|(names, _)| names.iter().any(|x| kind.eq_ignore_ascii_case(x)))
230 .into_iter()
231 .flat_map(|(_, v)| v.iter());
232 for entry in entries {
233 dot_ignore.write_all(entry)?;
234 dot_ignore.write_all(b"\n")?;
235 }
236 }
237 Ok(())
238}