libpijul_compat/
fs_representation.rs

1//! Layout of a repository (files in `.pijul`) on the disk. This
2//! module exports both high-level functions that require no knowledge
3//! of the repository, and lower-level constants documented on
4//! [pijul.org/documentation/repository](https://pijul.org/documentation/repository),
5//! used for instance for downloading files from remote repositories.
6
7use backend::DEFAULT_BRANCH;
8use backend::{Hash, HashRef, MutTxn, ROOT_INODE};
9use bs58;
10use flate2;
11use ignore::overrides::OverrideBuilder;
12use ignore::WalkBuilder;
13use patch::{Patch, PatchHeader};
14use rand;
15use rand::Rng;
16use std;
17use std::collections::HashSet;
18use std::fs::canonicalize;
19use std::fs::{create_dir_all, metadata, File};
20use std::io::{BufReader, Read, Write};
21use std::path::{Path, PathBuf};
22use Result;
23
24/// Name of the root directory, i.e. `.pijul`.
25pub const PIJUL_DIR_NAME: &'static str = ".pijul";
26
27/// Concatenate the parameter with `PIJUL_DIR_NAME`.
28pub fn repo_dir<P: AsRef<Path>>(p: P) -> PathBuf {
29    p.as_ref().join(PIJUL_DIR_NAME)
30}
31
32/// Directory where the pristine is, from the root of the repository.
33/// For instance, if the repository in in `/a/b`,
34/// `pristine_dir("/a/b") returns `/a/b/.pijul/pristine`.
35pub fn pristine_dir<P: AsRef<Path>>(p: P) -> PathBuf {
36    return p.as_ref().join(PIJUL_DIR_NAME).join("pristine");
37}
38
39/// Directory where the patches are. `patches_dir("/a/b") = "/a/b/.pijul/patches"`.
40pub fn patches_dir<P: AsRef<Path>>(p: P) -> PathBuf {
41    return p.as_ref().join(PIJUL_DIR_NAME).join("patches");
42}
43
44/// Basename of the changes file for branch `br`. This file is only
45/// used when pulling/pushing over HTTP (where calling remote programs
46/// to list patches is impossible).
47///
48/// The changes file contains the same information as the one returned by `pijul log --hash-only`.
49pub fn branch_changes_base_path(b: &str) -> String {
50    "changes.".to_string() + &bs58::encode(b.as_bytes()).into_string()
51}
52
53/// Changes file from the repository root and branch name.
54pub fn branch_changes_file(p: &Path, b: &str) -> PathBuf {
55    p.join(PIJUL_DIR_NAME).join(branch_changes_base_path(b))
56}
57
58/// The meta file, where user preferences are stored.
59pub fn meta_file(p: &Path) -> PathBuf {
60    p.join(PIJUL_DIR_NAME).join("meta.toml")
61}
62
63/// The id file is used for remote operations, to identify a
64/// repository and save bandwidth when the remote state is partially
65/// known.
66pub fn id_file(p: &Path) -> PathBuf {
67    p.join(PIJUL_DIR_NAME).join("id")
68}
69
70/// Find the repository root from one of its descendant
71/// directories. Return `None` iff `dir` is not in a repository.
72pub fn find_repo_root<'a>(dir: &'a Path) -> Option<PathBuf> {
73    let mut p = dir.to_path_buf();
74    loop {
75        p.push(PIJUL_DIR_NAME);
76        match metadata(&p) {
77            Ok(ref attr) if attr.is_dir() => {
78                p.pop();
79                return Some(p);
80            }
81            _ => {}
82        }
83        p.pop();
84
85        if !p.pop() {
86            return None;
87        }
88    }
89}
90
91#[doc(hidden)]
92pub const ID_LENGTH: usize = 100;
93
94/// Create a repository. `dir` must be the repository root (a
95/// `".pijul"` directory will be created in `dir`).
96pub fn create<R: Rng>(dir: &Path, mut rng: R) -> std::io::Result<()> {
97    debug!("create: {:?}", dir);
98    let mut repo_dir = repo_dir(dir);
99    create_dir_all(&repo_dir)?;
100
101    repo_dir.push("pristine");
102    create_dir_all(&repo_dir)?;
103    repo_dir.pop();
104
105    repo_dir.push("patches");
106    create_dir_all(&repo_dir)?;
107    repo_dir.pop();
108
109    repo_dir.push("id");
110    let mut f = std::fs::File::create(&repo_dir)?;
111    let mut x = String::new();
112    x.extend(rng.gen_ascii_chars().take(ID_LENGTH));
113    f.write_all(x.as_bytes())?;
114    repo_dir.pop();
115
116    repo_dir.push("version");
117    let mut f = std::fs::File::create(&repo_dir)?;
118    writeln!(f, "{}", env!("CARGO_PKG_VERSION"))?;
119    repo_dir.pop();
120
121    repo_dir.push("local");
122    create_dir_all(&repo_dir)?;
123    repo_dir.pop();
124
125    repo_dir.push("hooks");
126    create_dir_all(&repo_dir)?;
127    repo_dir.pop();
128
129    repo_dir.push("local/ignore");
130    let _f = std::fs::File::create(&repo_dir)?;
131    repo_dir.pop();
132
133    Ok(())
134}
135
136/// Basename of the patch corresponding to the given patch hash.
137pub fn patch_file_name(hash: HashRef) -> String {
138    hash.to_base58() + ".gz"
139}
140
141/// Read a complete patch.
142pub fn read_patch(repo: &Path, hash: HashRef) -> Result<Patch> {
143    let patch_dir = patches_dir(repo);
144    let path = patch_dir.join(&patch_file_name(hash));
145    debug!("read_patch, reading from {:?}", path);
146    let f = File::open(path)?;
147    let mut f = BufReader::new(f);
148    let (_, _, patch) = Patch::from_reader_compressed(&mut f)?;
149    Ok(patch)
150}
151
152/// Read a patch, but without the "changes" part, i.e. the actual
153/// contents of the patch.
154pub fn read_patch_nochanges(repo: &Path, hash: HashRef) -> Result<PatchHeader> {
155    let patch_dir = patches_dir(repo);
156    let path = patch_dir.join(&patch_file_name(hash));
157    debug!("read_patch_nochanges, reading from {:?}", path);
158    let f = File::open(path)?;
159    let mut f = flate2::bufread::GzDecoder::new(BufReader::new(f));
160    Ok(PatchHeader::from_reader_nochanges(&mut f)?)
161}
162
163/// Read a patch, but without the "changes" part, i.e. the actual
164/// contents of the patch.
165pub fn read_dependencies(repo: &Path, hash: HashRef) -> Result<Vec<Hash>> {
166    let patch_dir = patches_dir(repo);
167    let path = patch_dir.join(&patch_file_name(hash));
168    debug!("read_patch_nochanges, reading from {:?}", path);
169    let f = File::open(path)?;
170    let mut f = flate2::bufread::GzDecoder::new(BufReader::new(f));
171    Ok(Patch::read_dependencies(&mut f)?)
172}
173
174pub fn ignore_file(repo_root: &Path) -> PathBuf {
175    repo_root.join(PIJUL_DIR_NAME).join("local").join("ignore")
176}
177
178pub fn untracked_files<T: rand::Rng>(txn: &MutTxn<T>, repo_root: &Path) -> HashSet<PathBuf> {
179    let known_files = txn.list_files(ROOT_INODE).unwrap_or_else(|_| vec![]);
180
181    let o = OverrideBuilder::new(repo_root)
182        .add("!.pijul")
183        .unwrap()
184        .build()
185        .unwrap(); // we can be pretty confident these two calls will
186                   // not fail as the glob is hard-coded
187
188    let mut w = WalkBuilder::new(repo_root);
189    w.git_ignore(false)
190        .git_exclude(false)
191        .git_global(false)
192        .hidden(false);
193
194    // add .pijul/local/ignore
195    w.add_ignore(ignore_file(repo_root));
196    w.overrides(o);
197
198    let mut ret = HashSet::<PathBuf>::new();
199    for f in w.build() {
200        if let Ok(f) = f {
201            let p = f.path();
202            if p == repo_root {
203                continue;
204            }
205            let pb = p.to_path_buf();
206
207            if let Ok(stripped) = p.strip_prefix(repo_root) {
208                if known_files.iter().any(|t| *t == stripped) {
209                    continue;
210                }
211            }
212            ret.insert(pb);
213        }
214    }
215    ret
216}
217
218pub fn get_current_branch(root: &Path) -> Result<String> {
219    debug!("path: {:?}", root);
220    let mut path = repo_dir(&canonicalize(root)?);
221    path.push("current_branch");
222    if let Ok(mut f) = File::open(&path) {
223        let mut s = String::new();
224        f.read_to_string(&mut s)?;
225        Ok(s.trim().to_string())
226    } else {
227        Ok(DEFAULT_BRANCH.to_string())
228    }
229}
230
231pub fn set_current_branch(root: &Path, branch: &str) -> Result<()> {
232    debug!("set current branch: root={:?}, branch={:?}", root, branch);
233    let mut path = repo_dir(&canonicalize(root)?);
234    path.push("current_branch");
235    let mut f = File::create(&path)?;
236    f.write_all(branch.trim().as_ref())?;
237    f.write_all(b"\n")?;
238    Ok(())
239}