1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! `mmz --import <dir>`: carry cache records over from another checkout.
//!
//! Delegated work runs in a sibling git worktree, which keeps its own
//! gitignored cache. Landing it makes this checkout the same tree, yet every
//! rule still reads stale here, because the passes were recorded next door. A
//! record's input digest folds repo-relative paths and content hashes (see
//! [`crate::hashing::digest_all`]), never an absolute path, so a record is as
//! valid in a twin checkout as where it was written. Importing copies records
//! and judges none of them: one recorded against other inputs still reads stale
//! here, so an import can never make a stale rule look fresh.
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::manifest::Manifest;
use crate::{cache, prune};
/// Imports into the checkout governing `cwd` the cache records of the checkout
/// at `from` (absolute, or relative to `cwd`), returning a one-line summary of
/// how many records moved. The rules are not listed: a landing hook runs this
/// on every land, and a line per rule is noise in the caller's context.
///
/// A record is taken when its rule exists in this checkout's merged manifest
/// and the record held here is not newer. Everything else is skipped silently.
///
/// # Errors
///
/// Returns [`Error::NoManifest`] or a manifest error when this checkout's
/// manifest cannot be loaded, [`Error::ImportNotCheckout`] when `from` has no
/// `.mmz/` directory, [`Error::ImportSelf`] when it resolves to this checkout,
/// or [`Error::CacheIo`] when a record cannot be listed or written.
pub fn import(cwd: &Path, from: &Path) -> Result<String> {
let located = Manifest::locate(cwd)?;
let manifest = &located.manifest;
let base = located.root.as_path();
let source = checkout(cwd, from, base)?;
let live = prune::live_identities(manifest, base)?;
let imported = cache::import(
&source.join(&manifest.cache_dir),
&base.join(&manifest.cache_dir),
&live,
)?;
let count = imported.len();
let noun = if count == 1 { "record" } else { "records" };
Ok(format!(
"mmz: imported {count} {noun} from {}\n",
from.display()
))
}
/// Resolves `from` to the canonical directory of another checkout.
///
/// Canonical on both sides, so a symlink, a `.` or a trailing slash cannot
/// disguise this checkout as another.
fn checkout(cwd: &Path, from: &Path, base: &Path) -> Result<PathBuf> {
let not_checkout = || Error::ImportNotCheckout {
dir: from.to_path_buf(),
};
let dir = cwd.join(from).canonicalize().map_err(|_| not_checkout())?;
if !dir.join(".mmz").is_dir() {
return Err(not_checkout());
}
if base.canonicalize().is_ok_and(|own| own == dir) {
return Err(Error::ImportSelf {
dir: from.to_path_buf(),
});
}
Ok(dir)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::import;
use crate::cache::{self, Outcome};
use crate::clock::Clock;
use crate::error::Error;
/// A checkout holding a one-rule manifest, with `sh` recorded at `ran_at`
/// when given.
fn checkout(ran_at: Option<u64>) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let config = dir.path().join(".mmz");
std::fs::create_dir_all(&config).expect("mkdir .mmz");
std::fs::write(config.join("config.yaml"), "commands:\n - name: sh\n")
.expect("write manifest");
if let Some(stamp) = ran_at {
cache::write(
&config.join("cache"),
"sh",
Clock::pinned(stamp),
&Outcome {
digest: "d",
ok: true,
..Outcome::default()
},
);
}
dir
}
#[test]
fn prints_only_the_count_with_a_plural_that_agrees() {
let here = checkout(None);
let other = checkout(Some(10));
let text = import(here.path(), other.path()).expect("import");
assert!(text.starts_with("mmz: imported 1 record from"), "{text}");
assert_eq!(text.lines().count(), 1, "no per-rule line: {text}");
let again = import(here.path(), other.path()).expect("import");
assert!(
again.starts_with("mmz: imported 0 records from"),
"nothing left: {again}"
);
}
#[test]
fn a_relative_directory_is_read_from_the_working_directory() {
let here = checkout(None);
let other = checkout(Some(10));
// Both tempdirs sit in one parent, so the other is a sibling.
let name = other.path().file_name().expect("tempdir name");
let relative = Path::new("..").join(name);
let text = import(here.path(), &relative).expect("import");
assert!(text.contains("imported 1 record from"), "resolved: {text}");
}
#[test]
fn refuses_a_bare_directory_and_this_checkout() {
let here = checkout(None);
let bare = tempfile::tempdir().expect("tempdir");
assert!(matches!(
import(here.path(), bare.path()),
Err(Error::ImportNotCheckout { .. })
));
assert!(matches!(
import(here.path(), here.path()),
Err(Error::ImportSelf { .. })
));
}
}