pijul_repository/
lib.rs

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"];
23// Static KV map of names for project kinds |-> elements
24// that should go in the `.ignore` file by default.
25const 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
182/// Create and populate an initial `.ignore` file for the repository.
183/// The default elements are defined in the constant [`DEFAULT_IGNORE`].
184fn 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    // Don't replace/modify an existing `.ignore` file.
193    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
210/// if `kind` matches any of the known project kinds, add the associated
211/// .ignore entries to the default `.ignore` file.
212fn 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}