OXYGENHUB

Binary files' current versions found without whole closures 7fc365369758

live_replaces decided which replacement of a binary file is live by
building each replacing change's whole closure. On VS Code's history
(166k commits) each is ~100k changes, the bounded closure memo evicted
them constantly, and oxy git verify spent a day rebuilding them (perf:
90% in State::closure and SipHash inserts). It now asks the bounded walk
DepOracle::depends_on, which keeps nothing and answers the same.

refs #107
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-12 01:06 UTC
Signature
✔ signed by Andy McCall
Hash
7fc365369758e86cee8fd05e469ace7b3285d7ce8b6779b12414e62a854e7a7f
Depends on
34fad4a92451 62794ecc7f74 8fd78eb52ad2 b689779e9e74 ff0af1ee299d
Depended on by
ebb9210bad0c 31d3e932e97a 5d79b5c2de88
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.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
5 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified CHANGELOG.md +2 −0
…
1 unchanged lines
2 2
3 3
## Unreleased
4 4
5 +
- **Binary files' current versions found without whole closures (#107).** Which replacement of a binary file is live was decided by building each replacing change's entire closure; on a history as long as VS Code's (166,000 commits) each is a hundred thousand changes, the memo could not keep them, and `oxy git verify` spent its day rebuilding them. A bounded walk that keeps nothing answers the same.
6 +
5 7
- **Large new files are not snapshotted unseen, and past snapshots expire (#98).** A 1 GB image left untracked in a working tree was taken in by the next `status` and kept by the operation log for good: deleting the file and running `gc --now` freed nothing. Now a new, untracked file over `snapshot.max-new-file-size` (default 100 MiB) is left out and named by `status`, with how to ignore it or raise the limit; tracked files and files already in the working copy are unaffected. Past working-copy snapshots keep their files only for `core.snapshot-retention` (default 7 days), so `gc` prunes what only they held, and `oxy gc --expire-snapshots --now` does it at once. `undo` and `op restore` to an operation whose working copy has expired keep the files as they are.
6 8
7 9
- **`oxy undo` undoes this workspace's last command (#99).** It undid the repository's newest operation, whichever workspace made it: in a repository with several workspaces an undo could take back another workspace's commit or merge, and it said only which operation it went back to. Operations now record their workspace, and undo takes back this workspace's last command, leaving other workspaces' work as it is. On a bookmark both share, only its own heads go; a change another workspace built on it, or the workspace it would take away, refuses the undo and names it. Undo now says which command it undid and what that changed, passes over pushes (the remote keeps what it was sent), and warns when an undone commit is already on a remote. `oxy amend` warns when it rewrites a change a remote already has, so the next push will need `--force`.
…
172 unchanged lines
modified crates/oxy-core/src/state.rs +7 −5
…
1913 unchanged lines
1914 1914
        let Some(rs) = self.replaces.get(node) else {
1915 1915
            return vec![];
1916 1916
        };
1917 +
        // Dependence by the bounded walk rather than whole closures: on a
1918 +
        // long history a closure is a hundred thousand changes, and the
1919 +
        // memo cannot keep them all (#107).
1920 +
        let dep =
1921 +
            |a: &ChangeHash, b: &ChangeHash| a != b && <Self as DepOracle>::depends_on(self, a, b);
1917 1922
        let knows_its_old = |s: &ChangeHash, old: &ObjectRef| {
1918 1923
            rs.iter()
1919 -
                .any(|(q, _, n)| n.as_ref() == Some(old) && q != s && self.closure(s).contains(q))
1924 +
                .any(|(q, _, n)| n.as_ref() == Some(old) && q != s && dep(s, q))
1920 1925
        };
1921 1926
        rs.iter()
1922 1927
            .filter_map(|(c, _, n)| n.map(|n| (*c, n)))
1923 1928
            .filter(|(c, n)| {
1924 1929
                !rs.iter().any(|(s, o, _)| {
1925 -
                    s != c
1926 -
                        && *o == Some(*n)
1927 -
                        && (self.closure(s).contains(c)
1928 -
                            || (!knows_its_old(s, n) && !self.closure(c).contains(s)))
1930 +
                    s != c && *o == Some(*n) && (dep(s, c) || (!knows_its_old(s, n) && !dep(c, s)))
1929 1931
                })
1930 1932
            })
1931 1933
            .collect()
…
1010 unchanged lines