|
|
… |
40 unchanged lines |
| 41 |
41 |
|
let _wc = self.lock_working_copy()?; |
| 42 |
42 |
|
let _all = crate::timing::phase("checkout"); |
| 43 |
43 |
|
// A working copy clean by the manifest needs no snapshot; otherwise a |
| 44 |
|
- |
// status snapshot, which neither reads nor renders files the |
| 45 |
|
- |
// manifest proves unchanged, and whose base is history only |
| 46 |
|
- |
// (oxygen#35). |
| 47 |
|
- |
let (base, changed_paths) = if self.clean_by_manifest()? { |
| 48 |
|
- |
let (_, wc) = self.working_copy()?; |
| 49 |
|
- |
(self.load_state_history(&wc.deps)?, Vec::new()) |
|
44 |
+ |
// status snapshot records what changed, and rewrites the manifest |
|
45 |
+ |
// with the graph key of every file it proves unchanged. Either way |
|
46 |
+ |
// the manifest then says, for each file on disk, which graph it |
|
47 |
+ |
// renders, so the base state itself is not needed (oxygen#35). |
|
48 |
+ |
let changed_paths: Vec<String> = if self.clean_by_manifest()? { |
|
49 |
+ |
Vec::new() |
| 50 |
50 |
|
} else { |
| 51 |
51 |
|
let snap = self.snapshot_status(command)?; |
| 52 |
52 |
|
self.refuse_dirty(&snap, discard)?; |
| 53 |
|
- |
let paths = snap.statuses.iter().map(|s| s.path.clone()).collect(); |
| 54 |
|
- |
(snap.base, paths) |
|
53 |
+ |
snap.statuses.iter().map(|s| s.path.clone()).collect() |
| 55 |
54 |
|
}; |
| 56 |
|
- |
let views = crate::timing::phase("checkout: manifest and base view"); |
|
55 |
+ |
let (_, wc_now) = self.working_copy()?; |
| 57 |
56 |
|
let old_manifest = self.read_manifest()?.map(|(_, _, m)| m).unwrap_or_default(); |
| 58 |
|
- |
let bview = base.tree().view(); |
| 59 |
|
- |
drop(views); |
| 60 |
57 |
|
let changed: HashSet<&str> = changed_paths.iter().map(String::as_str).collect(); |
| 61 |
58 |
|
// The target's history; its file graphs are built a batch at a time |
| 62 |
59 |
|
// for the files that must be written, and dropped after. |
|
|
… |
9 unchanged lines |
| 72 |
69 |
|
.problems(tview.entries.keys().map(String::as_str)), |
| 73 |
70 |
|
); |
| 74 |
71 |
|
report.conflicted = tview.is_conflicted(); |
| 75 |
|
- |
report.lacks = base.changes().filter(|(h, _)| !target.contains(h)).count(); |
|
72 |
+ |
report.lacks = self.count_lacking(&wc_now.deps, &target)?; |
| 76 |
73 |
|
|
| 77 |
74 |
|
// Remove what the base or the working copy has and the target does |
| 78 |
75 |
|
// not. A nested repository is never deleted (spec §7.8). |
| 79 |
76 |
|
let mut dirs_to_prune: BTreeSet<PathBuf> = BTreeSet::new(); |
| 80 |
|
- |
let gone: BTreeSet<&str> = bview |
|
77 |
+ |
let gone: BTreeSet<&str> = old_manifest |
| 81 |
78 |
|
.entries |
| 82 |
|
- |
.iter() |
| 83 |
|
- |
.filter(|(_, f)| f.kind != NodeKind::Dir) |
| 84 |
|
- |
.map(|(p, _)| p.as_str()) |
|
79 |
+ |
.keys() |
|
80 |
+ |
.map(String::as_str) |
| 85 |
81 |
|
.chain(changed.iter().copied()) |
| 86 |
82 |
|
.filter(|p| !tview.entries.contains_key(*p)) |
| 87 |
83 |
|
.collect(); |
| 88 |
84 |
|
for path in gone { |
| 89 |
|
- |
if bview |
| 90 |
|
- |
.entries |
| 91 |
|
- |
.get(path) |
| 92 |
|
- |
.is_some_and(|f| f.kind == NodeKind::Subrepo) |
| 93 |
|
- |
{ |
|
85 |
+ |
let dir = self.root.join(path); |
|
86 |
+ |
if dir.join(OXY_DIR).exists() || dir.join(".git").exists() { |
| 94 |
87 |
|
report |
| 95 |
88 |
|
.notes |
| 96 |
89 |
|
.push(format!("{path}: nested repository left in place")); |
|
|
… |
20 unchanged lines |
| 117 |
110 |
|
if e.kind == NodeKind::Dir || !self.in_scope(path) { |
| 118 |
111 |
|
continue; |
| 119 |
112 |
|
} |
| 120 |
|
- |
let untouched = !changed.contains(path.as_str()) |
| 121 |
|
- |
&& bview |
| 122 |
|
- |
.entries |
| 123 |
|
- |
.get(path) |
| 124 |
|
- |
.is_some_and(|b| b.node == e.node && b.kind == e.kind && b.mode == e.mode); |
|
113 |
+ |
// The manifest entry of a file the working copy has not changed, |
|
114 |
+ |
// with the right executable bit. |
|
115 |
+ |
let exec_ok = |
|
116 |
+ |
|m: &prompt::ManifestEntry| !self.platform.exec_bits || m.exec == e.mode.exec; |
|
117 |
+ |
let entry = old_manifest |
|
118 |
+ |
.entries |
|
119 |
+ |
.get(path) |
|
120 |
+ |
.filter(|m| !changed.contains(path.as_str()) && exec_ok(m)); |
| 125 |
121 |
|
if e.kind == NodeKind::File && !e.mode.opaque { |
| 126 |
|
- |
// The manifest proves the file on disk equal to the base's |
| 127 |
|
- |
// rendering (its key is the base graph's, and its stat was |
| 128 |
|
- |
// checked just now), and the graph is the same in both |
| 129 |
|
- |
// states: nothing to read, render or write. |
| 130 |
|
- |
let key = base.graph_key(&e.node); |
| 131 |
|
- |
let same_graph = untouched |
| 132 |
|
- |
&& !is_attributes(path) |
| 133 |
|
- |
&& key.is_some() |
| 134 |
|
- |
&& key == target.graph_key(&e.node); |
|
122 |
+ |
// The manifest proves the file on disk equal to the rendering |
|
123 |
+ |
// of a graph whose key is the target's: nothing to read, |
|
124 |
+ |
// render or write. |
|
125 |
+ |
let key = target.graph_key(&e.node).map(|k| ObjectRef(k).to_hex()); |
| 135 |
126 |
|
let proven = |m: &&prompt::ManifestEntry| { |
| 136 |
|
- |
same_graph |
| 137 |
|
- |
&& m.graph.as_deref() == key.map(|k| ObjectRef(k).to_hex()).as_deref() |
|
127 |
+ |
!is_attributes(path) && key.is_some() && m.graph == key |
| 138 |
128 |
|
}; |
| 139 |
|
- |
match old_manifest.entries.get(path).filter(proven) { |
|
129 |
+ |
match entry.filter(proven) { |
| 140 |
130 |
|
Some(m) => { |
| 141 |
131 |
|
entries.insert( |
| 142 |
132 |
|
path.clone(), |
|
|
… |
1 unchanged lines |
| 144 |
134 |
|
content_hash: Some(m.hash.clone()), |
| 145 |
135 |
|
graph_key: m.graph.clone(), |
| 146 |
136 |
|
unchanged: true, |
|
137 |
+ |
stat: Some((m.size, m.mtime_ns)), |
| 147 |
138 |
|
..FsEntry::new(NodeKind::File, e.mode.exec, Vec::new()) |
| 148 |
139 |
|
}, |
| 149 |
140 |
|
); |
|
|
… |
25 unchanged lines |
| 175 |
166 |
|
report.notes.extend(sync.note(path)); |
| 176 |
167 |
|
continue; |
| 177 |
168 |
|
} |
| 178 |
|
- |
let same_blob = untouched && base.live_blobs(&e.node) == blobs; |
| 179 |
|
- |
if let Some(m) = old_manifest |
| 180 |
|
- |
.entries |
| 181 |
|
- |
.get(path) |
| 182 |
|
- |
.filter(|_| same_blob && blobs.len() == 1) |
|
169 |
+ |
if let Some(m) = entry |
|
170 |
+ |
.filter(|m| e.kind == NodeKind::File && blobs.len() == 1 && m.blob == Some(blob)) |
| 183 |
171 |
|
{ |
| 184 |
172 |
|
entries.insert( |
| 185 |
173 |
|
path.clone(), |
| 186 |
174 |
|
FsEntry { |
| 187 |
175 |
|
content_hash: Some(m.hash.clone()), |
| 188 |
176 |
|
blob: (e.kind == NodeKind::File).then_some(blob), |
|
177 |
+ |
stat: Some((m.size, m.mtime_ns)), |
| 189 |
178 |
|
..FsEntry::new(e.kind, e.mode.exec, Vec::new()) |
| 190 |
179 |
|
}, |
| 191 |
180 |
|
); |
|
|
… |
141 unchanged lines |
| 333 |
322 |
|
), |
| 334 |
323 |
|
); |
| 335 |
324 |
|
Ok(report) |
|
325 |
+ |
} |
|
326 |
+ |
|
|
327 |
+ |
/// How many changes of the state at `from` the state `to` lacks: a walk |
|
328 |
+ |
/// down from `from` that stops at what `to` holds. |
|
329 |
+ |
fn count_lacking(&self, from: &[ChangeHash], to: &State) -> Result<usize> { |
|
330 |
+ |
let mut seen: HashSet<ChangeHash> = HashSet::new(); |
|
331 |
+ |
let mut stack: Vec<ChangeHash> = from.to_vec(); |
|
332 |
+ |
while let Some(h) = stack.pop() { |
|
333 |
+ |
if to.contains(&h) || !seen.insert(h) { |
|
334 |
+ |
continue; |
|
335 |
+ |
} |
|
336 |
+ |
stack.extend(self.store.get_change_lazy(&h)?.0.deps); |
|
337 |
+ |
} |
|
338 |
+ |
Ok(seen.len()) |
| 336 |
339 |
|
} |
| 337 |
340 |
|
|
| 338 |
341 |
|
/// Refuse to leave uncommitted work behind unless `discard`. |
|
|
… |
109 unchanged lines |