A Git import commit costs what it changes 2306a4d24779
A commit's tree is now its first parent's with the commit's diff applied, read from one git diff-tree process fed every pair up front, instead of a git ls-tree of the whole tree per commit. An ordinary commit in a tree without submodules is built from the paths it changes (with the .oxyattributes files and the directories its removals may empty): only those are rendered, read and compared. A parent whose state is not kept has its tree listed, so its child still renders only what changed. Unchanged opaque files and symlinks are no longer read and stored again. The tree keeps its live placements and modes as an index, so its view walks what is live rather than every placement the history ever had. The text diff sets aside lines found on one side only before running Myers, as Git's xdiff does, and compares lines as numbers: a file rewritten whole (line endings changed) no longer costs quadratic time. It diffs from the base's rendering rather than rendering it again. The first 5 000 vscode commits import in 81 s (from 312 s) and verify clean. Repeated phases report their totals (OXY_TIMINGS). refs #25
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-30 06:39 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
2306a4d24779c0d3590b409951ad0eb8c557512a83a58a4ba412597d913449c3- Depends on
- 01a47cd7f0c8 03571027793e 0bdc3f91a39f 206c76cad4bf 252e8cb23fbc 30ea9c1e70fa 4dee277c327a 614541c3cebd 62794ecc7f74 6e723b81f5b9 75167672ebcd 7891e21c762f 79ab8528ff62 958fb27dda7c 982864068a59 a87c7b9d563a ab4d4e31b38d b493d993a9df c3069537d7c5 cdb5beba7f05 dfb23a3a8cfa e7ddd552b619 ff0af1ee299d
- Depended on by
- c2e1d09047db d6e0d5b8b339 aedfe465b611 f0406c3b40ed ce118287d9da 73d592e763ab c3d2ef6104c2 360aa74a6277 fd280228bdc2 05059d434aca 23ff1288a5dd f9575918477f 40a40bb7b798 e83354cdac22
- In bookmarks
- agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 63 atoms, 0 tree operations, 6 files
Browse the tree at this change
modified crates/oxy-core/src/diff.rs +70 −63
| … | 6 unchanged lines |
||
| 7 | 7 | use crate::ids::NodeId; |
|
| 8 | 8 | use crate::objects::{AtomRange, AtomRef, Edit}; |
|
| 9 | 9 | use similar::DiffOp; |
|
| 10 | + | use std::collections::HashMap; |
|
| 10 | 11 | ||
| 11 | 12 | /// Split into lines, each keeping its terminator; a final unterminated line |
|
| 12 | 13 | /// is its own atom. |
|
| … | 28 unchanged lines |
||
| 41 | 42 | out |
|
| 42 | 43 | } |
|
| 43 | 44 | ||
| 45 | + | /// The lines `old` and `new` keep in common, as (old index, new index) in |
|
| 46 | + | /// order: a longest common subsequence, found by Myers' algorithm. A line |
|
| 47 | + | /// found on one side only cannot be in it, so such lines are set aside |
|
| 48 | + | /// before the diff, as Git's xdiff does: a file rewritten whole (every |
|
| 49 | + | /// line ending changed, say) then costs time linear in its length rather |
|
| 50 | + | /// than quadratic. Lines are compared as small numbers, not bytes. |
|
| 51 | + | fn kept_lines<'a>(old: &[&'a [u8]], new: &[&'a [u8]]) -> Vec<(usize, usize)> { |
|
| 52 | + | let mut ids: HashMap<&'a [u8], u32> = HashMap::new(); |
|
| 53 | + | let mut intern = |l: &'a [u8]| -> u32 { |
|
| 54 | + | let next = ids.len() as u32; |
|
| 55 | + | *ids.entry(l).or_insert(next) |
|
| 56 | + | }; |
|
| 57 | + | let o: Vec<u32> = old.iter().map(|l| intern(l)).collect(); |
|
| 58 | + | let n: Vec<u32> = new.iter().map(|l| intern(l)).collect(); |
|
| 59 | + | let mut in_old = vec![false; ids.len()]; |
|
| 60 | + | let mut in_new = vec![false; ids.len()]; |
|
| 61 | + | for &x in &o { |
|
| 62 | + | in_old[x as usize] = true; |
|
| 63 | + | } |
|
| 64 | + | for &x in &n { |
|
| 65 | + | in_new[x as usize] = true; |
|
| 66 | + | } |
|
| 67 | + | let oi: Vec<usize> = (0..o.len()).filter(|&i| in_new[o[i] as usize]).collect(); |
|
| 68 | + | let ni: Vec<usize> = (0..n.len()).filter(|&j| in_old[n[j] as usize]).collect(); |
|
| 69 | + | let os: Vec<u32> = oi.iter().map(|&i| o[i]).collect(); |
|
| 70 | + | let ns: Vec<u32> = ni.iter().map(|&j| n[j]).collect(); |
|
| 71 | + | let mut capture = similar::algorithms::Capture::new(); |
|
| 72 | + | let _ = similar::algorithms::myers::diff(&mut capture, &os, 0..os.len(), &ns, 0..ns.len()); |
|
| 73 | + | let mut kept = Vec::new(); |
|
| 74 | + | for op in capture.into_ops() { |
|
| 75 | + | if let DiffOp::Equal { |
|
| 76 | + | old_index, |
|
| 77 | + | new_index, |
|
| 78 | + | len, |
|
| 79 | + | } = op |
|
| 80 | + | { |
|
| 81 | + | kept.extend((0..len).map(|k| (oi[old_index + k], ni[new_index + k]))); |
|
| 82 | + | } |
|
| 83 | + | } |
|
| 84 | + | kept |
|
| 85 | + | } |
|
| 86 | + | ||
| 44 | 87 | /// Edits that transform the live lines `old` of `node` into `new`. |
|
| 45 | 88 | pub fn text_edits(node: NodeId, old: &[(AtomKey, Vec<u8>)], new: &[u8]) -> Vec<Edit> { |
|
| 89 | + | let keys: Vec<AtomKey> = old.iter().map(|(k, _)| *k).collect(); |
|
| 46 | 90 | let old_lines: Vec<&[u8]> = old.iter().map(|(_, l)| l.as_slice()).collect(); |
|
| 91 | + | text_edits_of(node, &keys, &old_lines, new) |
|
| 92 | + | } |
|
| 93 | + | ||
| 94 | + | /// `text_edits` for live lines given as their atoms and, apart, their |
|
| 95 | + | /// text (`old[i]` is the text of `keys[i]`). |
|
| 96 | + | pub fn text_edits_of(node: NodeId, keys: &[AtomKey], old_lines: &[&[u8]], new: &[u8]) -> Vec<Edit> { |
|
| 47 | 97 | let new_lines = split_lines(new); |
|
| 48 | - | // Myers ops in file order. `capture_diff_slices` also runs the `Compact` |
|
| 49 | - | // hook, which slides deletions along runs of equal lines and can leave |
|
| 50 | - | // an insertion with the old index of a line before an equal one: the |
|
| 51 | - | // insertion was then anchored a line too early and landed on the wrong |
|
| 52 | - | // side of it (found by verifying a real Git import commit by commit). |
|
| 53 | - | let mut hook = similar::algorithms::Replace::new(similar::algorithms::Capture::new()); |
|
| 54 | - | let _ = similar::algorithms::diff_deadline( |
|
| 55 | - | similar::Algorithm::Myers, |
|
| 56 | - | &mut hook, |
|
| 57 | - | &old_lines, |
|
| 58 | - | 0..old_lines.len(), |
|
| 59 | - | &new_lines, |
|
| 60 | - | 0..new_lines.len(), |
|
| 61 | - | None, |
|
| 62 | - | ); |
|
| 63 | - | let ops = hook.into_inner().into_ops(); |
|
| 64 | 98 | ||
| 65 | 99 | let anchor = |i: usize| -> (AtomRef, AtomRef) { |
|
| 66 | 100 | let after = if i == 0 { |
|
| 67 | 101 | AtomRef::Start |
|
| 68 | 102 | } else { |
|
| 69 | 103 | AtomRef::Atom { |
|
| 70 | - | change: old[i - 1].0 .0, |
|
| 71 | - | index: old[i - 1].0 .1, |
|
| 104 | + | change: keys[i - 1].0, |
|
| 105 | + | index: keys[i - 1].1, |
|
| 72 | 106 | } |
|
| 73 | 107 | }; |
|
| 74 | - | let before = if i >= old.len() { |
|
| 108 | + | let before = if i >= keys.len() { |
|
| 75 | 109 | AtomRef::End |
|
| 76 | 110 | } else { |
|
| 77 | 111 | AtomRef::Atom { |
|
| 78 | - | change: old[i].0 .0, |
|
| 79 | - | index: old[i].0 .1, |
|
| 112 | + | change: keys[i].0, |
|
| 113 | + | index: keys[i].1, |
|
| 80 | 114 | } |
|
| 81 | 115 | }; |
|
| 82 | 116 | (after, before) |
|
| 83 | 117 | }; |
|
| 84 | 118 | ||
| 119 | + | // Each run of lines between two kept ones is deleted, and the new |
|
| 120 | + | // lines there are inserted where the first deleted line was. |
|
| 85 | 121 | let mut deleted: Vec<AtomKey> = Vec::new(); |
|
| 86 | 122 | let mut inserts: Vec<Edit> = Vec::new(); |
|
| 87 | - | for op in ops { |
|
| 88 | - | match op { |
|
| 89 | - | DiffOp::Equal { .. } => {} |
|
| 90 | - | DiffOp::Delete { |
|
| 91 | - | old_index, old_len, .. |
|
| 92 | - | } => { |
|
| 93 | - | deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k)); |
|
| 94 | - | } |
|
| 95 | - | DiffOp::Insert { |
|
| 96 | - | old_index, |
|
| 97 | - | new_index, |
|
| 98 | - | new_len, |
|
| 99 | - | } => { |
|
| 100 | - | let (after, before) = anchor(old_index); |
|
| 101 | - | inserts.push(Edit::Insert { |
|
| 102 | - | node, |
|
| 103 | - | after, |
|
| 104 | - | before, |
|
| 105 | - | lines: new_lines[new_index..new_index + new_len] |
|
| 106 | - | .iter() |
|
| 107 | - | .copied() |
|
| 108 | - | .collect(), |
|
| 109 | - | }); |
|
| 110 | - | } |
|
| 111 | - | DiffOp::Replace { |
|
| 112 | - | old_index, |
|
| 113 | - | old_len, |
|
| 114 | - | new_index, |
|
| 115 | - | new_len, |
|
| 116 | - | } => { |
|
| 117 | - | deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k)); |
|
| 118 | - | let (after, before) = anchor(old_index); |
|
| 119 | - | inserts.push(Edit::Insert { |
|
| 120 | - | node, |
|
| 121 | - | after, |
|
| 122 | - | before, |
|
| 123 | - | lines: new_lines[new_index..new_index + new_len] |
|
| 124 | - | .iter() |
|
| 125 | - | .copied() |
|
| 126 | - | .collect(), |
|
| 127 | - | }); |
|
| 128 | - | } |
|
| 123 | + | let (mut i, mut j) = (0, 0); |
|
| 124 | + | let end = (old_lines.len(), new_lines.len()); |
|
| 125 | + | for (ki, kj) in kept_lines(old_lines, &new_lines).into_iter().chain([end]) { |
|
| 126 | + | deleted.extend_from_slice(&keys[i..ki]); |
|
| 127 | + | if kj > j { |
|
| 128 | + | let (after, before) = anchor(i); |
|
| 129 | + | inserts.push(Edit::Insert { |
|
| 130 | + | node, |
|
| 131 | + | after, |
|
| 132 | + | before, |
|
| 133 | + | lines: new_lines[j..kj].iter().copied().collect(), |
|
| 134 | + | }); |
|
| 129 | 135 | } |
|
| 136 | + | (i, j) = (ki + 1, kj + 1); |
|
| 130 | 137 | } |
|
| 131 | 138 | let mut edits = Vec::new(); |
|
| 132 | 139 | if !deleted.is_empty() { |
|
| … | 198 unchanged lines |
modified crates/oxy-core/src/repo/git.rs +264 −28
| … | 74 unchanged lines |
||
| 75 | 75 | } |
|
| 76 | 76 | } |
|
| 77 | 77 | ||
| 78 | + | /// One path a commit changes against its first parent: its new mode and |
|
| 79 | + | /// blob, or `None` when the commit deletes it. |
|
| 80 | + | type TreeDiff = Vec<(String, Option<(String, String)>)>; |
|
| 81 | + | ||
| 82 | + | /// A running `git diff-tree --stdin`, fed every `commit parent` pair up |
|
| 83 | + | /// front, whose diffs are read in the same order as they are needed: a |
|
| 84 | + | /// commit's tree is its first parent's plus its diff, at a cost that |
|
| 85 | + | /// follows what it changes rather than the whole tree (oxygen#25). |
|
| 86 | + | struct DiffTree { |
|
| 87 | + | child: std::process::Child, |
|
| 88 | + | stdout: std::io::BufReader<std::process::ChildStdout>, |
|
| 89 | + | feeder: Option<std::thread::JoinHandle<()>>, |
|
| 90 | + | /// The header read at the end of the previous diff. |
|
| 91 | + | next: Option<String>, |
|
| 92 | + | } |
|
| 93 | + | ||
| 94 | + | impl DiffTree { |
|
| 95 | + | fn start(git_dir: &Path, pairs: Vec<(String, String)>) -> Result<DiffTree> { |
|
| 96 | + | let mut child = Command::new("git") |
|
| 97 | + | .arg("-C") |
|
| 98 | + | .arg(git_dir) |
|
| 99 | + | .args([ |
|
| 100 | + | "diff-tree", |
|
| 101 | + | "--stdin", |
|
| 102 | + | "--always", |
|
| 103 | + | "-r", |
|
| 104 | + | "-z", |
|
| 105 | + | "--no-renames", |
|
| 106 | + | ]) |
|
| 107 | + | .stdin(Stdio::piped()) |
|
| 108 | + | .stdout(Stdio::piped()) |
|
| 109 | + | .stderr(Stdio::null()) |
|
| 110 | + | .spawn() |
|
| 111 | + | .map_err(|e| RepoError::Msg(format!("running git diff-tree: {e}")))?; |
|
| 112 | + | let mut stdin = child.stdin.take().unwrap(); |
|
| 113 | + | let stdout = std::io::BufReader::new(child.stdout.take().unwrap()); |
|
| 114 | + | // A thread writes, so git never waits on a full pipe for us to read. |
|
| 115 | + | let feeder = std::thread::spawn(move || { |
|
| 116 | + | let mut w = std::io::BufWriter::new(&mut stdin); |
|
| 117 | + | for (c, p) in pairs { |
|
| 118 | + | if writeln!(w, "{c} {p}").is_err() { |
|
| 119 | + | return; |
|
| 120 | + | } |
|
| 121 | + | } |
|
| 122 | + | let _ = w.flush(); |
|
| 123 | + | }); |
|
| 124 | + | Ok(DiffTree { |
|
| 125 | + | child, |
|
| 126 | + | stdout, |
|
| 127 | + | feeder: Some(feeder), |
|
| 128 | + | next: None, |
|
| 129 | + | }) |
|
| 130 | + | } |
|
| 131 | + | ||
| 132 | + | fn field(&mut self) -> Result<Option<String>> { |
|
| 133 | + | use std::io::BufRead; |
|
| 134 | + | let mut buf = Vec::new(); |
|
| 135 | + | if self.stdout.read_until(0, &mut buf)? == 0 { |
|
| 136 | + | return Ok(None); |
|
| 137 | + | } |
|
| 138 | + | if buf.last() == Some(&0) { |
|
| 139 | + | buf.pop(); |
|
| 140 | + | } |
|
| 141 | + | Ok(Some(String::from_utf8_lossy(&buf).into_owned())) |
|
| 142 | + | } |
|
| 143 | + | ||
| 144 | + | /// The diff of `sha` against the parent it was fed with. |
|
| 145 | + | fn diff(&mut self, sha: &str) -> Result<TreeDiff> { |
|
| 146 | + | let header = match self.next.take() { |
|
| 147 | + | Some(h) => Some(h), |
|
| 148 | + | None => self.field()?, |
|
| 149 | + | }; |
|
| 150 | + | if header.as_deref() != Some(sha) { |
|
| 151 | + | return fail(format!("git diff-tree: expected {sha}, got {header:?}")); |
|
| 152 | + | } |
|
| 153 | + | let mut out = Vec::new(); |
|
| 154 | + | while let Some(f) = self.field()? { |
|
| 155 | + | let Some(meta) = f.strip_prefix(':') else { |
|
| 156 | + | self.next = Some(f); |
|
| 157 | + | break; |
|
| 158 | + | }; |
|
| 159 | + | let Some(path) = self.field()? else { |
|
| 160 | + | return fail("git diff-tree: record without a path"); |
|
| 161 | + | }; |
|
| 162 | + | // `oldmode newmode oldsha newsha status` |
|
| 163 | + | let m: Vec<&str> = meta.split(' ').collect(); |
|
| 164 | + | if m.len() != 5 { |
|
| 165 | + | return fail(format!("git diff-tree: bad record {f:?}")); |
|
| 166 | + | } |
|
| 167 | + | let entry = if m[4].starts_with('D') { |
|
| 168 | + | None |
|
| 169 | + | } else { |
|
| 170 | + | Some((m[1].to_string(), m[3].to_string())) |
|
| 171 | + | }; |
|
| 172 | + | out.push((path, entry)); |
|
| 173 | + | } |
|
| 174 | + | Ok(out) |
|
| 175 | + | } |
|
| 176 | + | } |
|
| 177 | + | ||
| 178 | + | impl Drop for DiffTree { |
|
| 179 | + | fn drop(&mut self) { |
|
| 180 | + | let _ = self.child.kill(); |
|
| 181 | + | let _ = self.child.wait(); |
|
| 182 | + | if let Some(f) = self.feeder.take() { |
|
| 183 | + | let _ = f.join(); |
|
| 184 | + | } |
|
| 185 | + | } |
|
| 186 | + | } |
|
| 187 | + | ||
| 78 | 188 | /// One commit as `git log` describes it. |
|
| 79 | 189 | struct GitCommit { |
|
| 80 | 190 | sha: String, |
|
| … | 534 unchanged lines |
||
| 615 | 725 | } |
|
| 616 | 726 | type Kept = (State, BTreeMap<String, (String, String)>); |
|
| 617 | 727 | let mut kept: HashMap<String, Kept> = HashMap::new(); |
|
| 728 | + | // Every commit to import that has a parent, diffed against its first. |
|
| 729 | + | let pairs: Vec<(String, String)> = commits |
|
| 730 | + | .iter() |
|
| 731 | + | .filter(|gc| { |
|
| 732 | + | !known.imported.contains_key(&gc.sha) && !known.exported.contains_key(&gc.sha) |
|
| 733 | + | }) |
|
| 734 | + | .filter_map(|gc| Some((gc.sha.clone(), gc.parents.first()?.clone()))) |
|
| 735 | + | .collect(); |
|
| 736 | + | let mut diffs = DiffTree::start(git_dir, pairs)?; |
|
| 737 | + | let mut rebuilt = 0usize; |
|
| 618 | 738 | let mut kept_order: std::collections::VecDeque<String> = std::collections::VecDeque::new(); |
|
| 619 | 739 | let mut last: Option<String> = None; |
|
| 620 | 740 | let mut blob_cache: HashMap<String, Vec<u8>> = HashMap::new(); |
|
| … | 34 unchanged lines |
||
| 655 | 775 | // not the whole tree. |
|
| 656 | 776 | let mut st = State::new_history_only(); |
|
| 657 | 777 | self.add_git_ancestors(&mut st, git_dir, &[p1.as_str()], &known)?; |
|
| 658 | - | (st, None) |
|
| 778 | + | // The parent's tree, so this commit still |
|
| 779 | + | // renders only what it changes. |
|
| 780 | + | rebuilt += 1; |
|
| 781 | + | (st, Some(list_tree(git_dir, p1)?)) |
|
| 659 | 782 | } |
|
| 660 | 783 | } |
|
| 661 | 784 | } |
|
| … | 10 unchanged lines |
||
| 672 | 795 | self.add_git_ancestors_touching(&mut base, git_dir, &refs, &known, &mut touched)?; |
|
| 673 | 796 | } |
|
| 674 | 797 | lap(0, &mut clock); |
|
| 675 | - | let tree = list_tree(git_dir, &gc.sha)?; |
|
| 798 | + | // The commit's tree: its first parent's with the diff applied, |
|
| 799 | + | // or listed whole when that tree is not kept. |
|
| 800 | + | let diff = match gc.parents.first() { |
|
| 801 | + | Some(_) => Some(diffs.diff(&gc.sha)?), |
|
| 802 | + | None => None, |
|
| 803 | + | }; |
|
| 804 | + | let (tree, changed) = match (prev_tree, diff) { |
|
| 805 | + | (Some(mut t), Some(diff)) => { |
|
| 806 | + | let mut changed: HashSet<String> = HashSet::new(); |
|
| 807 | + | for (path, entry) in diff { |
|
| 808 | + | match entry { |
|
| 809 | + | Some(v) => { |
|
| 810 | + | t.insert(path.clone(), v); |
|
| 811 | + | } |
|
| 812 | + | None => { |
|
| 813 | + | t.remove(&path); |
|
| 814 | + | } |
|
| 815 | + | } |
|
| 816 | + | changed.insert(path); |
|
| 817 | + | } |
|
| 818 | + | (t, Some(changed)) |
|
| 819 | + | } |
|
| 820 | + | _ => (list_tree(git_dir, &gc.sha)?, None), |
|
| 821 | + | }; |
|
| 676 | 822 | lap(1, &mut clock); |
|
| 677 | 823 | // Text files with the same blob and mode as in the first |
|
| 678 | 824 | // parent's tree equal the base's rendering: they are neither |
|
| 679 | 825 | // rendered nor read nor compared (spec §12.3, as for a clean |
|
| 680 | 826 | // working copy), so a commit costs what it changes. |
|
| 681 | 827 | let mut same: HashMap<&str, NodeId> = HashMap::new(); |
|
| 682 | - | if let Some(prev) = &prev_tree { |
|
| 828 | + | // Opaque files and symlinks likewise, when the base has one |
|
| 829 | + | // blob for them: they are neither read nor stored again. |
|
| 830 | + | let mut same_opaque: HashSet<&str> = HashSet::new(); |
|
| 831 | + | // An ordinary commit in a tree without submodules is built from |
|
| 832 | + | // the paths it changes alone (oxygen#25): with the `.oxyattributes` |
|
| 833 | + | // files that decide how they are stored, and the directories its |
|
| 834 | + | // removals may leave empty. A merge, or a commit whose parent's |
|
| 835 | + | // tree is not at hand, is built from its whole tree. |
|
| 836 | + | let partial = changed.is_some() |
|
| 837 | + | && gc.parents.len() == 1 |
|
| 838 | + | && !tree.values().any(|(mode, _)| mode == "160000"); |
|
| 839 | + | let mut rel: BTreeSet<&str> = BTreeSet::new(); |
|
| 840 | + | let base_snapshot = if let (true, Some(changed)) = (partial, &changed) { |
|
| 683 | 841 | let view = base.tree().view(); |
|
| 684 | - | for (path, v) in &tree { |
|
| 685 | - | // Submodule handling reads these two whatever changed. |
|
| 686 | - | if prev.get(path) != Some(v) || path == ".gitmodules" || path == ".oxymodules" { |
|
| 687 | - | continue; |
|
| 842 | + | rel.extend(changed.iter().map(String::as_str)); |
|
| 843 | + | rel.extend( |
|
| 844 | + | tree.keys() |
|
| 845 | + | .map(String::as_str) |
|
| 846 | + | .filter(|p| *p == ".oxyattributes" || p.ends_with("/.oxyattributes")), |
|
| 847 | + | ); |
|
| 848 | + | let mut dirs: BTreeSet<&str> = BTreeSet::new(); |
|
| 849 | + | for p in changed.iter().filter(|p| !tree.contains_key(*p)) { |
|
| 850 | + | let mut end = p.len(); |
|
| 851 | + | while let Some(i) = p[..end].rfind('/') { |
|
| 852 | + | if !dirs.insert(&p[..i]) { |
|
| 853 | + | break; |
|
| 854 | + | } |
|
| 855 | + | end = i; |
|
| 688 | 856 | } |
|
| 689 | - | if let Some(e) = view.entries.get(path) { |
|
| 690 | - | if e.kind == NodeKind::File && !e.mode.opaque && !touched.contains(&e.node) |
|
| 857 | + | } |
|
| 858 | + | let need: Vec<NodeId> = rel |
|
| 859 | + | .iter() |
|
| 860 | + | .filter_map(|p| view.entries.get(*p)) |
|
| 861 | + | .filter(|e| e.kind == NodeKind::File && !e.mode.opaque) |
|
| 862 | + | .map(|e| e.node) |
|
| 863 | + | .collect(); |
|
| 864 | + | crate::timing::add("render: same and need", clock.elapsed()); |
|
| 865 | + | let t = std::time::Instant::now(); |
|
| 866 | + | self.load_graphs_of(&mut base, &need)?; |
|
| 867 | + | crate::timing::add("render: load graphs", t.elapsed()); |
|
| 868 | + | let t = std::time::Instant::now(); |
|
| 869 | + | let snap = base.materialize_paths(view, rel.iter().chain(dirs.iter()).copied()); |
|
| 870 | + | crate::timing::add("render: materialize", t.elapsed()); |
|
| 871 | + | snap |
|
| 872 | + | } else { |
|
| 873 | + | if let Some(changed) = &changed { |
|
| 874 | + | let view = base.tree().view(); |
|
| 875 | + | for path in tree.keys() { |
|
| 876 | + | // Submodule handling reads these two whatever changed. |
|
| 877 | + | if changed.contains(path) || path == ".gitmodules" || path == ".oxymodules" |
|
| 691 | 878 | { |
|
| 692 | - | same.insert(path.as_str(), e.node); |
|
| 879 | + | continue; |
|
| 880 | + | } |
|
| 881 | + | if let Some(e) = view.entries.get(path) { |
|
| 882 | + | if touched.contains(&e.node) { |
|
| 883 | + | continue; |
|
| 884 | + | } |
|
| 885 | + | let opaque = e.kind == NodeKind::Symlink |
|
| 886 | + | || (e.kind == NodeKind::File && e.mode.opaque); |
|
| 887 | + | if e.kind == NodeKind::File && !e.mode.opaque { |
|
| 888 | + | same.insert(path.as_str(), e.node); |
|
| 889 | + | } else if opaque && base.live_blobs(&e.node).len() == 1 { |
|
| 890 | + | same_opaque.insert(path.as_str()); |
|
| 891 | + | } |
|
| 693 | 892 | } |
|
| 694 | 893 | } |
|
| 695 | 894 | } |
|
| 696 | - | } |
|
| 697 | - | // Build the graphs of every text file the commit may change. |
|
| 698 | - | let skip: HashSet<NodeId> = same.values().copied().collect(); |
|
| 699 | - | let need: Vec<NodeId> = base |
|
| 700 | - | .tree() |
|
| 701 | - | .view() |
|
| 702 | - | .entries |
|
| 703 | - | .values() |
|
| 704 | - | .filter(|e| e.kind == NodeKind::File && !e.mode.opaque && !skip.contains(&e.node)) |
|
| 705 | - | .map(|e| e.node) |
|
| 706 | - | .collect(); |
|
| 707 | - | self.load_graphs_of(&mut base, &need)?; |
|
| 708 | - | let base_snapshot = base.materialize_except(&skip); |
|
| 895 | + | // Build the graphs of every text file the commit may change. |
|
| 896 | + | let skip: HashSet<NodeId> = same.values().copied().collect(); |
|
| 897 | + | let need: Vec<NodeId> = base |
|
| 898 | + | .tree() |
|
| 899 | + | .view() |
|
| 900 | + | .entries |
|
| 901 | + | .values() |
|
| 902 | + | .filter(|e| { |
|
| 903 | + | e.kind == NodeKind::File && !e.mode.opaque && !skip.contains(&e.node) |
|
| 904 | + | }) |
|
| 905 | + | .map(|e| e.node) |
|
| 906 | + | .collect(); |
|
| 907 | + | crate::timing::add("render: same and need", clock.elapsed()); |
|
| 908 | + | let t = std::time::Instant::now(); |
|
| 909 | + | self.load_graphs_of(&mut base, &need)?; |
|
| 910 | + | crate::timing::add("render: load graphs", t.elapsed()); |
|
| 911 | + | let t = std::time::Instant::now(); |
|
| 912 | + | let snap = base.materialize_except(&skip); |
|
| 913 | + | crate::timing::add("render: materialize", t.elapsed()); |
|
| 914 | + | snap |
|
| 915 | + | }; |
|
| 709 | 916 | ||
| 710 | 917 | lap(2, &mut clock); |
|
| 711 | 918 | // The commit's tree as in-memory entries. |
|
| 712 | 919 | let mut entries: BTreeMap<String, FsEntry> = BTreeMap::new(); |
|
| 713 | 920 | let mut gitlinks: Vec<(String, String)> = Vec::new(); |
|
| 714 | - | for (path, (mode, sha)) in &tree { |
|
| 921 | + | let listed: Box<dyn Iterator<Item = (&String, &(String, String))>> = if partial { |
|
| 922 | + | Box::new(rel.iter().filter_map(|p| tree.get_key_value(*p))) |
|
| 923 | + | } else { |
|
| 924 | + | Box::new(tree.iter()) |
|
| 925 | + | }; |
|
| 926 | + | for (path, (mode, sha)) in listed { |
|
| 715 | 927 | let (path, sha) = (path.clone(), sha.clone()); |
|
| 716 | 928 | let (kind, exec) = match mode.as_str() { |
|
| 717 | 929 | "100644" | "100664" => (NodeKind::File, false), |
|
| … | 5 unchanged lines |
||
| 723 | 935 | } |
|
| 724 | 936 | _ => continue, |
|
| 725 | 937 | }; |
|
| 726 | - | if same.contains_key(path.as_str()) { |
|
| 938 | + | if same.contains_key(path.as_str()) || same_opaque.contains(path.as_str()) { |
|
| 727 | 939 | let mut e = FsEntry::new(kind, exec, Vec::new()); |
|
| 728 | 940 | e.unchanged = true; |
|
| 729 | 941 | entries.insert(path, e); |
|
| … | 63 unchanged lines |
||
| 793 | 1005 | // New nodes' identifiers derive from the commit, so importing the |
|
| 794 | 1006 | // same Git history twice gives the same changes (spec §23). |
|
| 795 | 1007 | let seed = *blake3::hash(format!("oxy git import\0{}", gc.sha).as_bytes()).as_bytes(); |
|
| 796 | - | let mut built = |
|
| 797 | - | self.build_from_entries_seeded(&base, &base_snapshot, &entries, seed)?; |
|
| 1008 | + | let mut built = if partial { |
|
| 1009 | + | // Whether the new tree has anything under `dir`. |
|
| 1010 | + | let dir_needed = |dir: &str| { |
|
| 1011 | + | let prefix = format!("{dir}/"); |
|
| 1012 | + | tree.range(prefix.clone()..) |
|
| 1013 | + | .next() |
|
| 1014 | + | .is_some_and(|(k, _)| k.starts_with(&prefix)) |
|
| 1015 | + | }; |
|
| 1016 | + | self.build_from_entries_seeded( |
|
| 1017 | + | &base, |
|
| 1018 | + | &base_snapshot, |
|
| 1019 | + | &entries, |
|
| 1020 | + | seed, |
|
| 1021 | + | Some(&dir_needed), |
|
| 1022 | + | )? |
|
| 1023 | + | } else { |
|
| 1024 | + | self.build_from_entries_seeded(&base, &base_snapshot, &entries, seed, None)? |
|
| 1025 | + | }; |
|
| 798 | 1026 | lap(4, &mut clock); |
|
| 799 | 1027 | // A file or submodule with several live blobs (branches that |
|
| 800 | 1028 | // replaced it independently) is resolved to the commit's blob: |
|
| … | 47 unchanged lines |
||
| 848 | 1076 | ChangeKind::Normal |
|
| 849 | 1077 | }, |
|
| 850 | 1078 | }; |
|
| 1079 | + | let t = std::time::Instant::now(); |
|
| 851 | 1080 | deps.extend(referenced_changes(&change, &base)); |
|
| 1081 | + | crate::timing::add("store: referenced", t.elapsed()); |
|
| 852 | 1082 | change.deps = deps.into_iter().collect(); |
|
| 853 | 1083 | change.normalize(); |
|
| 854 | 1084 | let hash = self.store.put_change(&change)?; |
|
| … | 3 unchanged lines |
||
| 858 | 1088 | heads_now.insert(hash); |
|
| 859 | 1089 | // Built graphs take its edits whole; files not built keep only |
|
| 860 | 1090 | // where its text is, read back from the store. |
|
| 1091 | + | let t = std::time::Instant::now(); |
|
| 861 | 1092 | let (skeleton, texts) = self.store.get_change_lazy(&hash)?; |
|
| 862 | 1093 | base.add_known_mixed(hash, change, skeleton, texts); |
|
| 1094 | + | crate::timing::add("store: add to state", t.elapsed()); |
|
| 863 | 1095 | known.imported.insert(gc.sha.clone(), hash); |
|
| 864 | 1096 | if waiting.get(gc.sha.as_str()).is_some_and(|n| *n > 0) { |
|
| 865 | 1097 | kept.insert(gc.sha.clone(), (base, tree)); |
|
| … | 10 unchanged lines |
||
| 876 | 1108 | } |
|
| 877 | 1109 | for (name, d) in [ |
|
| 878 | 1110 | "git import: bases", |
|
| 879 | - | "git import: ls-tree", |
|
| 1111 | + | "git import: tree", |
|
| 880 | 1112 | "git import: render base", |
|
| 881 | 1113 | "git import: read blobs", |
|
| 882 | 1114 | "git import: build change", |
|
| … | 3 unchanged lines |
||
| 886 | 1118 | .zip(spent) |
|
| 887 | 1119 | { |
|
| 888 | 1120 | crate::timing::record(name, d); |
|
| 1121 | + | } |
|
| 1122 | + | crate::timing::report_totals(); |
|
| 1123 | + | if crate::timing::enabled() { |
|
| 1124 | + | eprintln!("timing git import: {rebuilt} bases rebuilt from history"); |
|
| 889 | 1125 | } |
|
| 890 | 1126 | ||
| 891 | 1127 | // The bookmark: heads of the old state plus every new change. |
|
| … | 439 unchanged lines |
modified crates/oxy-core/src/repo/snapshot.rs +43 −9
| … | 131 unchanged lines |
||
| 132 | 132 | /// `build_from_entries` whose new nodes take identifiers derived from |
|
| 133 | 133 | /// `seed` and their paths instead of random ones, so the same input |
|
| 134 | 134 | /// always builds the same change (a Git import, spec §23). |
|
| 135 | + | /// |
|
| 136 | + | /// With `dir_needed`, the snapshot and entries hold only the paths the |
|
| 137 | + | /// import touches (and the directories a removal may leave empty); a |
|
| 138 | + | /// directory stays while `dir_needed` says the new tree has something |
|
| 139 | + | /// under it. |
|
| 135 | 140 | pub(crate) fn build_from_entries_seeded( |
|
| 136 | 141 | &self, |
|
| 137 | 142 | base: &State, |
|
| 138 | 143 | base_snapshot: &Snapshot, |
|
| 139 | 144 | fs_entries: &BTreeMap<String, FsEntry>, |
|
| 140 | 145 | seed: [u8; 32], |
|
| 146 | + | dir_needed: Option<&dyn Fn(&str) -> bool>, |
|
| 141 | 147 | ) -> Result<BuiltChange> { |
|
| 142 | 148 | self.build_from_entries_with( |
|
| 143 | 149 | base, |
|
| … | 1 unchanged lines |
||
| 145 | 151 | HashMap::new(), |
|
| 146 | 152 | fs_entries, |
|
| 147 | 153 | &HashSet::new(), |
|
| 148 | - | Some(seed), |
|
| 154 | + | Some(Import { seed, dir_needed }), |
|
| 149 | 155 | ) |
|
| 150 | 156 | } |
|
| 151 | 157 | ||
| … | 4 unchanged lines |
||
| 156 | 162 | reuse: HashMap<String, (NodeId, NodeKind)>, |
|
| 157 | 163 | fs_entries: &BTreeMap<String, FsEntry>, |
|
| 158 | 164 | resolve: &HashSet<String>, |
|
| 159 | - | seed: Option<[u8; 32]>, |
|
| 165 | + | import: Option<Import>, |
|
| 160 | 166 | ) -> Result<BuiltChange> { |
|
| 167 | + | let seed = import.as_ref().map(|i| i.seed); |
|
| 168 | + | let dir_needed = import.and_then(|i| i.dir_needed); |
|
| 169 | + | let mut laps = crate::timing::Laps::start(); |
|
| 161 | 170 | // Attributes come from the files being committed, so a new |
|
| 162 | 171 | // `.oxyattributes` applies to the same change that adds it. |
|
| 163 | 172 | let attrs = crate::attributes::Attributes::from_files( |
|
| … | 5 unchanged lines |
||
| 169 | 178 | let mut b = ChangeBuilder::new(base, base_snapshot, reuse).with_attributes(attrs); |
|
| 170 | 179 | b.resolve = resolve.clone(); |
|
| 171 | 180 | b.node_seed = seed; |
|
| 181 | + | laps.lap("build: builder"); |
|
| 172 | 182 | // Rename detection (spec §7.1, §23): a removed file and an added file |
|
| 173 | 183 | // of the same kind whose contents are at least half alike become one |
|
| 174 | 184 | // move of the same node, so identity survives across the rename. |
|
| … | 6 unchanged lines |
||
| 181 | 191 | .iter() |
|
| 182 | 192 | .map(|(old, new)| (old.as_str(), new.as_str())) |
|
| 183 | 193 | .collect(); |
|
| 194 | + | laps.lap("build: renames"); |
|
| 184 | 195 | for (path, fs_entry) in fs_entries { |
|
| 185 | 196 | match base_snapshot.files.get(path) { |
|
| 186 | 197 | None => match renamed_from.get(path.as_str()) { |
|
| … | 5 unchanged lines |
||
| 192 | 203 | Some(expected) => b.update_path(self, path, fs_entry, expected)?, |
|
| 193 | 204 | } |
|
| 194 | 205 | } |
|
| 206 | + | laps.lap("build: paths"); |
|
| 195 | 207 | // A directory stays while anything under it exists or is out of |
|
| 196 | 208 | // scope: the directories above each such path, gathered once. |
|
| 197 | 209 | let mut needed_dirs: HashSet<&str> = HashSet::new(); |
|
| … | 21 unchanged lines |
||
| 219 | 231 | { |
|
| 220 | 232 | continue; |
|
| 221 | 233 | } |
|
| 222 | - | let still_needed = |
|
| 223 | - | expected.kind == NodeKind::Dir && needed_dirs.contains(path.as_str()); |
|
| 234 | + | let still_needed = expected.kind == NodeKind::Dir |
|
| 235 | + | && match dir_needed { |
|
| 236 | + | Some(f) => f(path), |
|
| 237 | + | None => needed_dirs.contains(path.as_str()), |
|
| 238 | + | }; |
|
| 224 | 239 | if !still_needed { |
|
| 225 | 240 | b.remove_path(path, expected); |
|
| 226 | 241 | } |
|
| 227 | 242 | } |
|
| 243 | + | laps.lap("build: removals"); |
|
| 228 | 244 | if seed.is_some() { |
|
| 229 | 245 | b.retire_stray_placements(); |
|
| 230 | 246 | } |
|
| 247 | + | laps.lap("build: stray placements"); |
|
| 231 | 248 | let mut statuses = b.statuses; |
|
| 232 | 249 | statuses.sort_by(|a, b| a.path.cmp(&b.path)); |
|
| 233 | 250 | Ok(BuiltChange { |
|
| … | 1336 unchanged lines |
||
| 1570 | 1587 | ) -> Self { |
|
| 1571 | 1588 | let mut dir_nodes: HashMap<String, NodeId> = HashMap::new(); |
|
| 1572 | 1589 | dir_nodes.insert(String::new(), NodeId::ROOT); |
|
| 1573 | - | for (p, f) in &base_snapshot.files { |
|
| 1574 | - | if f.kind == NodeKind::Dir { |
|
| 1575 | - | dir_nodes.insert(p.clone(), f.node); |
|
| 1590 | + | for (p, e) in &base_snapshot.tree.entries { |
|
| 1591 | + | if e.kind == NodeKind::Dir { |
|
| 1592 | + | dir_nodes.insert(p.clone(), e.node); |
|
| 1576 | 1593 | } |
|
| 1577 | 1594 | } |
|
| 1578 | 1595 | ChangeBuilder { |
|
| … | 434 unchanged lines |
||
| 2013 | 2030 | }); |
|
| 2014 | 2031 | status = Some(ChangeStatus::Modified); |
|
| 2015 | 2032 | } else if bytes != &disk_text { |
|
| 2016 | - | let old = self.base.live_lines(&node); |
|
| 2017 | - | let edits = text_edits(node, &old, &disk_text); |
|
| 2033 | + | let mut laps = crate::timing::Laps::start(); |
|
| 2034 | + | // The base's rendering holds its live atoms in order, |
|
| 2035 | + | // one per line (it is not conflicted here). |
|
| 2036 | + | let keys = materialized.live_atoms(); |
|
| 2037 | + | let lines = crate::diff::split_lines(bytes); |
|
| 2038 | + | let edits = if keys.len() == lines.len() { |
|
| 2039 | + | crate::diff::text_edits_of(node, &keys, &lines, &disk_text) |
|
| 2040 | + | } else { |
|
| 2041 | + | text_edits(node, &self.base.live_lines(&node), &disk_text) |
|
| 2042 | + | }; |
|
| 2043 | + | laps.lap("build: text diff"); |
|
| 2018 | 2044 | if !edits.is_empty() { |
|
| 2019 | 2045 | self.edits.extend(edits); |
|
| 2020 | 2046 | status = Some(ChangeStatus::Modified); |
|
| … | 356 unchanged lines |
||
| 2377 | 2403 | /// `RENAME_LIMIT` candidate pairs only identical contents are paired. |
|
| 2378 | 2404 | const RENAME_THRESHOLD: u32 = 50; |
|
| 2379 | 2405 | const RENAME_LIMIT: usize = 10_000; |
|
| 2406 | + | ||
| 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], |
|
| 2412 | + | dir_needed: Option<&'a dyn Fn(&str) -> bool>, |
|
| 2413 | + | } |
|
| 2380 | 2414 | ||
| 2381 | 2415 | fn detect_renames( |
|
| 2382 | 2416 | base_snapshot: &Snapshot, |
|
| … | 101 unchanged lines |
modified crates/oxy-core/src/state.rs +19 −0
| … | 1224 unchanged lines |
||
| 1225 | 1225 | } |
|
| 1226 | 1226 | } |
|
| 1227 | 1227 | ||
| 1228 | + | /// A snapshot of only `paths` (those `view` has), `view` being this |
|
| 1229 | + | /// state's tree view: for a caller that works from what changed. The |
|
| 1230 | + | /// graphs of those text files must be loaded. |
|
| 1231 | + | pub fn materialize_paths<'p>( |
|
| 1232 | + | &self, |
|
| 1233 | + | view: crate::tree::TreeView, |
|
| 1234 | + | paths: impl IntoIterator<Item = &'p str>, |
|
| 1235 | + | ) -> Snapshot { |
|
| 1236 | + | let files = paths |
|
| 1237 | + | .into_iter() |
|
| 1238 | + | .filter_map(|p| Some((p.to_string(), self.file_state(view.entries.get(p)?)))) |
|
| 1239 | + | .collect(); |
|
| 1240 | + | Snapshot { |
|
| 1241 | + | tree: view, |
|
| 1242 | + | files, |
|
| 1243 | + | unrendered: BTreeSet::new(), |
|
| 1244 | + | } |
|
| 1245 | + | } |
|
| 1246 | + | ||
| 1228 | 1247 | fn file_state(&self, e: &crate::tree::TreeEntry) -> FileState { |
|
| 1229 | 1248 | { |
|
| 1230 | 1249 | let content = match e.kind { |
|
| … | 750 unchanged lines |
modified crates/oxy-core/src/timing.rs +44 −0
| … | 41 unchanged lines |
||
| 42 | 42 | } |
|
| 43 | 43 | } |
|
| 44 | 44 | } |
|
| 45 | + | ||
| 46 | + | static TOTALS: std::sync::Mutex<Vec<(&'static str, std::time::Duration)>> = |
|
| 47 | + | std::sync::Mutex::new(Vec::new()); |
|
| 48 | + | ||
| 49 | + | /// Add `d` to the running total for `name`, for a phase repeated many |
|
| 50 | + | /// times (once per imported commit, say); `report_totals` prints them. |
|
| 51 | + | pub fn add(name: &'static str, d: std::time::Duration) { |
|
| 52 | + | if !enabled() { |
|
| 53 | + | return; |
|
| 54 | + | } |
|
| 55 | + | let mut t = TOTALS.lock().unwrap_or_else(|e| e.into_inner()); |
|
| 56 | + | match t.iter_mut().find(|(n, _)| *n == name) { |
|
| 57 | + | Some((_, total)) => *total += d, |
|
| 58 | + | None => t.push((name, d)), |
|
| 59 | + | } |
|
| 60 | + | } |
|
| 61 | + | ||
| 62 | + | /// Print and clear the totals `add` gathered. |
|
| 63 | + | pub fn report_totals() { |
|
| 64 | + | if !enabled() { |
|
| 65 | + | return; |
|
| 66 | + | } |
|
| 67 | + | let t = std::mem::take(&mut *TOTALS.lock().unwrap_or_else(|e| e.into_inner())); |
|
| 68 | + | for (name, d) in t { |
|
| 69 | + | record(name, d); |
|
| 70 | + | } |
|
| 71 | + | } |
|
| 72 | + | ||
| 73 | + | /// Times from one point to the next, adding each interval to a total. |
|
| 74 | + | pub struct Laps(Option<Instant>); |
|
| 75 | + | ||
| 76 | + | impl Laps { |
|
| 77 | + | pub fn start() -> Laps { |
|
| 78 | + | Laps(enabled().then(Instant::now)) |
|
| 79 | + | } |
|
| 80 | + | /// Add the time since the last lap to `name`'s total. |
|
| 81 | + | pub fn lap(&mut self, name: &'static str) { |
|
| 82 | + | if let Some(t) = &mut self.0 { |
|
| 83 | + | let now = Instant::now(); |
|
| 84 | + | add(name, now - *t); |
|
| 85 | + | *t = now; |
|
| 86 | + | } |
|
| 87 | + | } |
|
| 88 | + | } |
modified crates/oxy-core/src/tree.rs +59 −12
| … | 28 unchanged lines |
||
| 29 | 29 | deleted_modes: HashMap<OpKey, BTreeSet<ChangeHash>>, |
|
| 30 | 30 | placements_of: HashMap<NodeId, BTreeSet<OpKey>>, |
|
| 31 | 31 | modes_of: HashMap<NodeId, BTreeSet<OpKey>>, |
|
| 32 | + | /// The live subsets of `placements_of` and `modes_of`, kept as changes |
|
| 33 | + | /// apply, so the view walks what is live rather than all history. |
|
| 34 | + | live_placements_of: HashMap<NodeId, BTreeSet<OpKey>>, |
|
| 35 | + | live_modes_of: HashMap<NodeId, BTreeSet<OpKey>>, |
|
| 32 | 36 | /// The view, once computed; cleared by every change to the graph. |
|
| 33 | 37 | view_cache: std::sync::OnceLock<TreeView>, |
|
| 34 | 38 | } |
|
| … | 77 unchanged lines |
||
| 112 | 116 | }, |
|
| 113 | 117 | ); |
|
| 114 | 118 | self.placements_of.entry(*node).or_default().insert(key); |
|
| 119 | + | self.index_placement(key); |
|
| 115 | 120 | if *kind != NodeKind::Dir { |
|
| 116 | 121 | self.modes.insert(key, (*node, *mode)); |
|
| 117 | 122 | self.modes_of.entry(*node).or_default().insert(key); |
|
| 123 | + | self.index_mode(key); |
|
| 118 | 124 | } |
|
| 119 | 125 | } |
|
| 120 | 126 | TreeOp::RemoveNode { placement, .. } => { |
|
| … | 1 unchanged lines |
||
| 122 | 128 | .entry(op_key(*placement)) |
|
| 123 | 129 | .or_default() |
|
| 124 | 130 | .insert(change); |
|
| 131 | + | self.index_placement(op_key(*placement)); |
|
| 125 | 132 | } |
|
| 126 | 133 | TreeOp::Move { |
|
| 127 | 134 | node, |
|
| … | 5 unchanged lines |
||
| 133 | 140 | .entry(op_key(*old)) |
|
| 134 | 141 | .or_default() |
|
| 135 | 142 | .insert(change); |
|
| 143 | + | self.index_placement(op_key(*old)); |
|
| 136 | 144 | self.placements.insert( |
|
| 137 | 145 | key, |
|
| 138 | 146 | Placement { |
|
| … | 3 unchanged lines |
||
| 142 | 150 | }, |
|
| 143 | 151 | ); |
|
| 144 | 152 | self.placements_of.entry(*node).or_default().insert(key); |
|
| 153 | + | self.index_placement(key); |
|
| 145 | 154 | } |
|
| 146 | 155 | TreeOp::SetMode { node, old, mode } => { |
|
| 147 | 156 | self.deleted_modes |
|
| 148 | 157 | .entry(op_key(*old)) |
|
| 149 | 158 | .or_default() |
|
| 150 | 159 | .insert(change); |
|
| 160 | + | self.index_mode(op_key(*old)); |
|
| 151 | 161 | self.modes.insert(key, (*node, *mode)); |
|
| 152 | 162 | self.modes_of.entry(*node).or_default().insert(key); |
|
| 163 | + | self.index_mode(key); |
|
| 164 | + | } |
|
| 165 | + | } |
|
| 166 | + | } |
|
| 167 | + | ||
| 168 | + | /// Bring placement `k` in the live index up to date. |
|
| 169 | + | fn index_placement(&mut self, k: OpKey) { |
|
| 170 | + | let Some(node) = self.placements.get(&k).map(|p| p.node) else { |
|
| 171 | + | return; |
|
| 172 | + | }; |
|
| 173 | + | if self.placement_live(&k) { |
|
| 174 | + | self.live_placements_of.entry(node).or_default().insert(k); |
|
| 175 | + | } else if let Some(set) = self.live_placements_of.get_mut(&node) { |
|
| 176 | + | set.remove(&k); |
|
| 177 | + | if set.is_empty() { |
|
| 178 | + | self.live_placements_of.remove(&node); |
|
| 153 | 179 | } |
|
| 154 | 180 | } |
|
| 155 | 181 | } |
|
| 156 | 182 | ||
| 183 | + | /// Bring mode atom `k` in the live index up to date. |
|
| 184 | + | fn index_mode(&mut self, k: OpKey) { |
|
| 185 | + | let Some(node) = self.modes.get(&k).map(|(n, _)| *n) else { |
|
| 186 | + | return; |
|
| 187 | + | }; |
|
| 188 | + | if self.mode_live(&k) { |
|
| 189 | + | self.live_modes_of.entry(node).or_default().insert(k); |
|
| 190 | + | } else if let Some(set) = self.live_modes_of.get_mut(&node) { |
|
| 191 | + | set.remove(&k); |
|
| 192 | + | if set.is_empty() { |
|
| 193 | + | self.live_modes_of.remove(&node); |
|
| 194 | + | } |
|
| 195 | + | } |
|
| 196 | + | } |
|
| 197 | + | ||
| 157 | 198 | /// Directories no longer placed anywhere, by the (parent, name) they |
|
| 158 | 199 | /// were last placed at, with that placement: what an import can bring |
|
| 159 | 200 | /// back rather than make a second node for one Git path. |
|
| … | 61 unchanged lines |
||
| 221 | 262 | } |
|
| 222 | 263 | ||
| 223 | 264 | pub fn live_placements(&self, node: &NodeId) -> Vec<(OpKey, &Placement)> { |
|
| 224 | - | self.placements_of |
|
| 265 | + | self.live_placements_of |
|
| 225 | 266 | .get(node) |
|
| 226 | 267 | .into_iter() |
|
| 227 | 268 | .flat_map(|ks| ks.iter()) |
|
| 228 | - | .filter(|k| self.placement_live(k)) |
|
| 229 | 269 | .filter_map(|k| self.placements.get(k).map(|p| (*k, p))) |
|
| 230 | 270 | .collect() |
|
| 231 | 271 | } |
|
| 232 | 272 | ||
| 233 | 273 | pub fn live_modes(&self, node: &NodeId) -> Vec<(OpKey, Mode)> { |
|
| 234 | - | self.modes_of |
|
| 274 | + | self.live_modes_of |
|
| 235 | 275 | .get(node) |
|
| 236 | 276 | .into_iter() |
|
| 237 | 277 | .flat_map(|ks| ks.iter()) |
|
| 238 | - | .filter(|k| self.mode_live(k)) |
|
| 239 | 278 | .filter_map(|k| self.modes.get(k).map(|(_, m)| (*k, *m))) |
|
| 240 | 279 | .collect() |
|
| 241 | 280 | } |
|
| 242 | 281 | ||
| 243 | 282 | /// Every node that has at least one live placement. |
|
| 244 | 283 | pub fn placed_nodes(&self) -> impl Iterator<Item = NodeId> + '_ { |
|
| 245 | - | self.placements_of |
|
| 246 | - | .keys() |
|
| 247 | - | .copied() |
|
| 248 | - | .filter(|n| !self.live_placements(n).is_empty()) |
|
| 284 | + | self.live_placements_of.keys().copied() |
|
| 249 | 285 | } |
|
| 250 | 286 | ||
| 251 | 287 | /// Resolve the tree: assign paths, find conflicts. |
|
| … | 8 unchanged lines |
||
| 260 | 296 | ||
| 261 | 297 | // Live placements grouped by parent. |
|
| 262 | 298 | let mut children: HashMap<NodeId, Vec<(OpKey, &Placement)>> = HashMap::new(); |
|
| 263 | - | for (k, p) in &self.placements { |
|
| 264 | - | if self.placement_live(k) { |
|
| 265 | - | children.entry(p.parent).or_default().push((*k, p)); |
|
| 299 | + | for ks in self.live_placements_of.values() { |
|
| 300 | + | for k in ks { |
|
| 301 | + | if let Some(p) = self.placements.get(k) { |
|
| 302 | + | children.entry(p.parent).or_default().push((*k, p)); |
|
| 303 | + | } |
|
| 266 | 304 | } |
|
| 267 | 305 | } |
|
| 268 | 306 | for c in children.values_mut() { |
|
| … | 1 unchanged lines |
||
| 270 | 308 | } |
|
| 271 | 309 | ||
| 272 | 310 | // Multi-placement and mode conflicts. |
|
| 273 | - | let mut nodes: Vec<NodeId> = self.placements_of.keys().copied().collect(); |
|
| 311 | + | // Only placed nodes: one with no live placement is not in the tree. |
|
| 312 | + | let mut nodes: Vec<NodeId> = self.live_placements_of.keys().copied().collect(); |
|
| 274 | 313 | nodes.sort(); |
|
| 275 | 314 | let mut multi: HashSet<NodeId> = HashSet::new(); |
|
| 276 | 315 | for n in &nodes { |
|
| … | 276 unchanged lines |
||
| 553 | 592 | g.modes_of.entry(n).or_default().insert(k); |
|
| 554 | 593 | } |
|
| 555 | 594 | g.deleted_modes = deleters(field(m, 4, "deleted modes")?, "deleted mode")?; |
|
| 595 | + | let keys: Vec<OpKey> = g.placements.keys().copied().collect(); |
|
| 596 | + | for k in keys { |
|
| 597 | + | g.index_placement(k); |
|
| 598 | + | } |
|
| 599 | + | let keys: Vec<OpKey> = g.modes.keys().copied().collect(); |
|
| 600 | + | for k in keys { |
|
| 601 | + | g.index_mode(k); |
|
| 602 | + | } |
|
| 556 | 603 | Ok(g) |
|
| 557 | 604 | } |
|
| 558 | 605 | } |
|
| … | 326 unchanged lines |