|
|
… |
125 unchanged lines |
| 126 |
126 |
|
fs_entries: &BTreeMap<String, FsEntry>, |
| 127 |
127 |
|
resolve: &HashSet<String>, |
| 128 |
128 |
|
) -> Result<BuiltChange> { |
| 129 |
|
- |
self.build_from_entries_with(base, base_snapshot, reuse, fs_entries, resolve, None) |
|
129 |
+ |
self.build_from_entries_with( |
|
130 |
+ |
base, |
|
131 |
+ |
base_snapshot, |
|
132 |
+ |
reuse, |
|
133 |
+ |
fs_entries, |
|
134 |
+ |
resolve, |
|
135 |
+ |
Scope::default(), |
|
136 |
+ |
) |
| 130 |
137 |
|
} |
| 131 |
138 |
|
|
| 132 |
139 |
|
/// `build_from_entries` whose new nodes take identifiers derived from |
|
|
… |
18 unchanged lines |
| 151 |
158 |
|
HashMap::new(), |
| 152 |
159 |
|
fs_entries, |
| 153 |
160 |
|
&HashSet::new(), |
| 154 |
|
- |
Some(Import { seed, dir_needed }), |
|
161 |
+ |
Scope { |
|
162 |
+ |
seed: Some(seed), |
|
163 |
+ |
dir_needed, |
|
164 |
+ |
}, |
| 155 |
165 |
|
) |
| 156 |
166 |
|
} |
| 157 |
167 |
|
|
|
168 |
+ |
/// `build_from_entries` for a snapshot most of whose files are known |
|
169 |
+ |
/// unchanged (oxygen#41): only the paths that may differ are built |
|
170 |
+ |
/// (files read rather than known unchanged, base files gone from disk, |
|
171 |
+ |
/// and the directories those may leave empty), against a snapshot of |
|
172 |
+ |
/// just those, so a large tree with a few edits costs what they touch. |
|
173 |
+ |
fn build_changed_only( |
|
174 |
+ |
&self, |
|
175 |
+ |
base: &State, |
|
176 |
+ |
base_snapshot: &Snapshot, |
|
177 |
+ |
reuse: HashMap<String, (NodeId, NodeKind)>, |
|
178 |
+ |
fs_entries: &BTreeMap<String, FsEntry>, |
|
179 |
+ |
) -> Result<BuiltChange> { |
|
180 |
+ |
// A file known unchanged may still have changed mode: that alters |
|
181 |
+ |
// neither its size nor its time. |
|
182 |
+ |
let changed: BTreeMap<String, FsEntry> = fs_entries |
|
183 |
+ |
.iter() |
|
184 |
+ |
.filter(|(p, e)| { |
|
185 |
+ |
!e.unchanged |
|
186 |
+ |
|| base_snapshot |
|
187 |
+ |
.files |
|
188 |
+ |
.get(*p) |
|
189 |
+ |
.is_none_or(|f| f.kind != e.kind || f.mode.exec != e.exec) |
|
190 |
+ |
}) |
|
191 |
+ |
.map(|(p, e)| (p.clone(), e.clone())) |
|
192 |
+ |
.collect(); |
|
193 |
+ |
let mut files: BTreeMap<String, crate::state::FileState> = BTreeMap::new(); |
|
194 |
+ |
for p in changed.keys() { |
|
195 |
+ |
if let Some(f) = base_snapshot.files.get(p) { |
|
196 |
+ |
files.insert(p.clone(), f.clone()); |
|
197 |
+ |
} |
|
198 |
+ |
} |
|
199 |
+ |
for (p, f) in &base_snapshot.files { |
|
200 |
+ |
if f.kind == NodeKind::Dir || fs_entries.contains_key(p) { |
|
201 |
+ |
continue; |
|
202 |
+ |
} |
|
203 |
+ |
files.insert(p.clone(), f.clone()); |
|
204 |
+ |
let mut end = p.len(); |
|
205 |
+ |
while let Some(i) = p[..end].rfind('/') { |
|
206 |
+ |
let dir = &p[..i]; |
|
207 |
+ |
if files.contains_key(dir) { |
|
208 |
+ |
break; |
|
209 |
+ |
} |
|
210 |
+ |
if let Some(d) = base_snapshot.files.get(dir) { |
|
211 |
+ |
files.insert(dir.to_string(), d.clone()); |
|
212 |
+ |
} |
|
213 |
+ |
end = i; |
|
214 |
+ |
} |
|
215 |
+ |
} |
|
216 |
+ |
let part = Snapshot { |
|
217 |
+ |
tree: base_snapshot.tree.clone(), |
|
218 |
+ |
files, |
|
219 |
+ |
unrendered: BTreeSet::new(), |
|
220 |
+ |
}; |
|
221 |
+ |
// Whether anything on disk is under `dir`. |
|
222 |
+ |
let dir_needed = |dir: &str| { |
|
223 |
+ |
let prefix = format!("{dir}/"); |
|
224 |
+ |
fs_entries |
|
225 |
+ |
.range(prefix.clone()..) |
|
226 |
+ |
.next() |
|
227 |
+ |
.is_some_and(|(k, _)| k.starts_with(&prefix)) |
|
228 |
+ |
}; |
|
229 |
+ |
self.build_from_entries_with( |
|
230 |
+ |
base, |
|
231 |
+ |
&part, |
|
232 |
+ |
reuse, |
|
233 |
+ |
&changed, |
|
234 |
+ |
&HashSet::new(), |
|
235 |
+ |
Scope { |
|
236 |
+ |
seed: None, |
|
237 |
+ |
dir_needed: Some(&dir_needed), |
|
238 |
+ |
}, |
|
239 |
+ |
) |
|
240 |
+ |
} |
|
241 |
+ |
|
| 158 |
242 |
|
fn build_from_entries_with( |
| 159 |
243 |
|
&self, |
| 160 |
244 |
|
base: &State, |
|
|
… |
1 unchanged lines |
| 162 |
246 |
|
reuse: HashMap<String, (NodeId, NodeKind)>, |
| 163 |
247 |
|
fs_entries: &BTreeMap<String, FsEntry>, |
| 164 |
248 |
|
resolve: &HashSet<String>, |
| 165 |
|
- |
import: Option<Import>, |
|
249 |
+ |
scope: Scope, |
| 166 |
250 |
|
) -> Result<BuiltChange> { |
| 167 |
|
- |
let seed = import.as_ref().map(|i| i.seed); |
| 168 |
|
- |
let dir_needed = import.and_then(|i| i.dir_needed); |
|
251 |
+ |
let Scope { seed, dir_needed } = scope; |
| 169 |
252 |
|
let mut laps = crate::timing::Laps::start(); |
| 170 |
253 |
|
// Attributes come from the files being committed, so a new |
| 171 |
254 |
|
// `.oxyattributes` applies to the same change that adds it. |
|
|
… |
200 unchanged lines |
| 372 |
455 |
|
self.platform_normalize(&mut fs_entries, &base_snapshot); |
| 373 |
456 |
|
let built = { |
| 374 |
457 |
|
let _t = crate::timing::phase("snapshot: diff against base"); |
| 375 |
|
- |
self.build_from_entries(&base, &base_snapshot, reuse, &fs_entries)? |
|
458 |
+ |
if partial && self.config.sparse_paths.is_empty() { |
|
459 |
+ |
#[cfg(test)] |
|
460 |
+ |
let full = |
|
461 |
+ |
self.build_from_entries(&base, &base_snapshot, reuse.clone(), &fs_entries)?; |
|
462 |
+ |
let built = self.build_changed_only(&base, &base_snapshot, reuse, &fs_entries)?; |
|
463 |
+ |
#[cfg(test)] |
|
464 |
+ |
{ |
|
465 |
+ |
// New nodes take random identifiers: compare them by |
|
466 |
+ |
// order of appearance. |
|
467 |
+ |
let norm = |b: &BuiltChange| { |
|
468 |
+ |
let text = format!("{:?} {:?} {:?}", b.tree_ops, b.edits, b.statuses); |
|
469 |
+ |
let mut seen: Vec<String> = Vec::new(); |
|
470 |
+ |
let mut out = String::new(); |
|
471 |
+ |
let mut rest = text.as_str(); |
|
472 |
+ |
while let Some(i) = rest.find("NodeId(") { |
|
473 |
+ |
out.push_str(&rest[..i]); |
|
474 |
+ |
let end = i + rest[i..].find(')').unwrap() + 1; |
|
475 |
+ |
let id = rest[i..end].to_string(); |
|
476 |
+ |
let n = seen.iter().position(|x| *x == id).unwrap_or_else(|| { |
|
477 |
+ |
seen.push(id); |
|
478 |
+ |
seen.len() - 1 |
|
479 |
+ |
}); |
|
480 |
+ |
out.push_str(&format!("N{n}")); |
|
481 |
+ |
rest = &rest[end..]; |
|
482 |
+ |
} |
|
483 |
+ |
out.push_str(rest); |
|
484 |
+ |
out |
|
485 |
+ |
}; |
|
486 |
+ |
assert_eq!( |
|
487 |
+ |
norm(&built), |
|
488 |
+ |
norm(&full), |
|
489 |
+ |
"a changed-only build is the full one" |
|
490 |
+ |
); |
|
491 |
+ |
} |
|
492 |
+ |
built |
|
493 |
+ |
} else { |
|
494 |
+ |
self.build_from_entries(&base, &base_snapshot, reuse, &fs_entries)? |
|
495 |
+ |
} |
| 376 |
496 |
|
}; |
| 377 |
497 |
|
// Text files found equal to an unconflicted base rendering keep its |
| 378 |
498 |
|
// graph key, so the next snapshot need not read or render them. |
|
|
… |
2025 unchanged lines |
| 2404 |
2524 |
|
const RENAME_THRESHOLD: u32 = 50; |
| 2405 |
2525 |
|
const RENAME_LIMIT: usize = 10_000; |
| 2406 |
2526 |
|
|
| 2407 |
|
- |
/// What a Git import adds to building a change: the seed its new nodes' |
| 2408 |
|
- |
/// identifiers derive from, and, when it builds from part of the tree, |
| 2409 |
|
- |
/// whether the new tree has anything under a directory. |
| 2410 |
|
- |
struct Import<'a> { |
| 2411 |
|
- |
seed: [u8; 32], |
|
2527 |
+ |
/// How a change is built beyond its entries: for a Git import, the seed |
|
2528 |
+ |
/// its new nodes' identifiers derive from; and, when it is built from part |
|
2529 |
+ |
/// of the tree, whether the new tree has anything under a directory. |
|
2530 |
+ |
#[derive(Default)] |
|
2531 |
+ |
struct Scope<'a> { |
|
2532 |
+ |
seed: Option<[u8; 32]>, |
| 2412 |
2533 |
|
dir_needed: Option<&'a dyn Fn(&str) -> bool>, |
| 2413 |
2534 |
|
} |
| 2414 |
2535 |
|
|
|
|
… |
103 unchanged lines |