OXYGENHUB

git import: build a merge from the paths it can change ce118287d9da

A merge was built from its whole tree, rendering and comparing every
file: about 18 s for any merge at 118k commits of VS Code, 85% of the
import's time. A merge can differ from its first parent only at the
paths Git's diff names and where the merged changes touched, read in
the first parent's tree and in the merged state; with those it is built
like an ordinary commit. A tree conflict among what the merge touched
still takes the whole-tree path (OXY_IMPORT_WHOLE_MERGES=1 forces it,
for comparing). The trace says which path each merge took.

On the recovered VS Code copy small merges went from ~18 s to 13-139 ms;
large ones now cost what they rebuild (23-63 s for 43-308 files). Every
commit of four generated histories (210 merges, 29 conflicted)
reproduced by git verify. Not yet verified at VS Code depth: git verify
there ran out of memory beside the live import.

refs #80
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-06 23:27 UTC
Signature
✔ signed by Andy McCall
Hash
ce118287d9da03ef5c98345e3149109c48cf872b6813ec469ccdc2f269e6f34c
Depends on
2306a4d24779 58dfe0e134eb 79ab8528ff62 810691ca6ce6 c3069537d7c5 c3d2ef6104c2 fb5b6181618a
Depended on by
d256e826ac5d 579afd107b9b
In bookmarks
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.10.0 v0.11.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
12 atoms, 0 tree operations, 1 file

Browse the tree at this change

modified crates/oxy-core/src/repo/git.rs +69 −3
…
932 unchanged lines
933 933
        let stop_after: Option<usize> = std::env::var("OXY_IMPORT_STOP_AFTER")
934 934
            .ok()
935 935
            .and_then(|v| v.parse().ok());
936 +
        let whole_merges = std::env::var_os("OXY_IMPORT_WHOLE_MERGES").filter(|v| v != "0");
936 937
        let total = commits.len();
937 938
        for (index, gc) in commits.iter().enumerate() {
938 939
            if known.imported.contains_key(&gc.sha) || known.exported.contains_key(&gc.sha) {
…
62 unchanged lines
1001 1002
            // tree cannot vouch for them.
1002 1003
            let mut touched: HashSet<NodeId> = HashSet::new();
1003 1004
            let mut merged = 0usize;
1005 +
            // Where the first parent has each node, read before the merged
1006 +
            // changes move anything: a merge's partial build names the paths
1007 +
            // they touch in the first parent's tree as well as in the merged
1008 +
            // state (oxygen#80).
1009 +
            let mut first_parent_view: Option<crate::tree::TreeView> = None;
1004 1010
            if gc.parents.len() > 1 {
1005 1011
                report.merges += 1;
1012 +
                if prev_tree.is_some() && whole_merges.is_none() {
1013 +
                    first_parent_view = Some(base.tree().view());
1014 +
                }
1006 1015
                let mut args: Vec<String> = gc.parents[1..].to_vec();
1007 1016
                args.push(format!("^{}", gc.parents[0]));
1008 1017
                let refs: Vec<&str> = args.iter().map(String::as_str).collect();
…
54 unchanged lines
1063 1072
            // files that decide how they are stored, and the directories its
1064 1073
            // removals may leave empty. A merge, or a commit whose parent's
1065 1074
            // tree is not at hand, is built from its whole tree.
1075 +
            // A merge can differ from its first parent only at the paths
1076 +
            // Git's diff names and where the merged changes touched: those
1077 +
            // paths in the first parent's tree and in the merged state. With
1078 +
            // them added it is built like an ordinary commit, instead of
1079 +
            // from its whole tree (18 s for any merge at 118k commits of
1080 +
            // VS Code, oxygen#80). OXY_IMPORT_WHOLE_MERGES=1 restores the
1081 +
            // whole-tree path, for comparing.
1082 +
            let mut changed = changed;
1083 +
            // A tree conflict in the merged state (both sides placed a file
1084 +
            // at one path) is resolved by the whole-tree path.
1085 +
            let mut merge_how = if gc.parents.len() > 1 {
1086 +
                if first_parent_view.is_some() {
1087 +
                    "partial"
1088 +
                } else {
1089 +
                    "whole"
1090 +
                }
1091 +
            } else {
1092 +
                "-"
1093 +
            };
1094 +
            // Only a conflict among what the merged changes touched; one
1095 +
            // elsewhere stays as it was in the first parent either way.
1096 +
            if first_parent_view.is_some()
1097 +
                && base.tree().view().conflicts.iter().any(|c| {
1098 +
                    use crate::tree::TreeConflict::*;
1099 +
                    match c {
1100 +
                        Name { nodes, .. } => nodes.iter().any(|n| touched.contains(n)),
1101 +
                        MultiPlacement { node, .. } | Cycle { node } | Mode { node, .. } => {
1102 +
                            touched.contains(node)
1103 +
                        }
1104 +
                        Orphan { node, parent } => {
1105 +
                            touched.contains(node) || touched.contains(parent)
1106 +
                        }
1107 +
                    }
1108 +
                })
1109 +
            {
1110 +
                first_parent_view = None;
1111 +
                merge_how = "conflicted";
1112 +
            }
1113 +
            if let (Some(ch), Some(fp)) = (changed.as_mut(), &first_parent_view) {
1114 +
                let union = base.tree().view();
1115 +
                for v in [fp, &union] {
1116 +
                    for (path, e) in v.entries.iter() {
1117 +
                        if touched.contains(&e.node) {
1118 +
                            ch.insert(path.clone());
1119 +
                        }
1120 +
                    }
1121 +
                }
1122 +
            }
1066 1123
            let partial = changed.is_some()
1067 -
                && gc.parents.len() == 1
1124 +
                && (gc.parents.len() == 1 || first_parent_view.is_some())
1068 1125
                && !tree.values().any(|(mode, _)| mode == "160000");
1069 1126
            let mut rel: BTreeSet<&str> = BTreeSet::new();
1127 +
            // Text files whose graphs this commit builds, for the trace.
1128 +
            let need_n: usize;
1070 1129
            let base_snapshot = if let (true, Some(changed)) = (partial, &changed) {
1071 1130
                let view = base.tree().view();
1072 1131
                rel.extend(changed.iter().map(String::as_str));
…
19 unchanged lines
1092 1151
                    .map(|e| e.node)
1093 1152
                    .collect();
1094 1153
                crate::timing::add("render: same and need", clock.elapsed());
1154 +
                need_n = need.len();
1095 1155
                let t = std::time::Instant::now();
1096 1156
                self.load_graphs_of(&mut base, &need)?;
1097 1157
                crate::timing::add("render: load graphs", t.elapsed());
…
37 unchanged lines
1135 1195
                    .map(|e| e.node)
1136 1196
                    .collect();
1137 1197
                crate::timing::add("render: same and need", clock.elapsed());
1198 +
                need_n = need.len();
1138 1199
                let t = std::time::Instant::now();
1139 1200
                let snap = if need.len() > merge_batch() {
1140 1201
                    // A merge onto a base with no built graphs: thousands
1141 1202
                    // of files, whose graphs would not fit at once.
1142 1203
                    self.materialize_batched(&mut base, &need, &skip)?
1143 1204
                } else {
1205 +
                    let g = std::time::Instant::now();
1144 1206
                    self.load_graphs_of(&mut base, &need)?;
1145 -
                    base.materialize_except(&skip)
1207 +
                    crate::timing::add("render: whole, load graphs", g.elapsed());
1208 +
                    let m = std::time::Instant::now();
1209 +
                    let snap = base.materialize_except(&skip);
1210 +
                    crate::timing::add("render: whole, materialize", m.elapsed());
1211 +
                    snap
1146 1212
                };
1147 1213
                crate::timing::add("render: whole tree", t.elapsed());
1148 1214
                snap
…
227 unchanged lines
1376 1442
                    .map(|(st, _)| st.built_atoms())
1377 1443
                    .unwrap_or(0);
1378 1444
                eprintln!(
1379 -
                    "trace {done} {} parents={} base={base_how} merged={merged} built={built} atoms={atoms} kept={} shed={shed} rss_mb={} before_trim_mb={before_trim} deferred_edits={edits} most={most} ms={}",
1445 +
                    "trace {done} {} parents={} base={base_how} merged={merged} built={built} atoms={atoms} kept={} shed={shed} rss_mb={} before_trim_mb={before_trim} deferred_edits={edits} most={most} need={need_n} merge={merge_how} ms={}",
1380 1446
                    &gc.sha[..12.min(gc.sha.len())],
1381 1447
                    gc.parents.len(),
1382 1448
                    kept.len(),
…
554 unchanged lines