OXYGENHUB

Log H11 (Run pipeline from bookmarks and changes) as done 03571027793e

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-29 09:52 UTC
Signature
✔ signed by Andy McCall
Hash
03571027793ef0d317e42e6f329d823634af804c934dd34f396feb9fdbbc22ae
Depends on
28fee1c9eb6c 30ea9c1e70fa 4960d87af58b 4dee277c327a 75036530b621 79ab8528ff62 c3069537d7c5 e15dc29f109b
Depended on by
fb5b6181618a 2306a4d24779 b493d993a9df a5d2fbe7c10d fd3f920f0b24 958fb27dda7c
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
13 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified crates/oxy-core/src/repo/git.rs +47 −11
…
593 unchanged lines
594 594
        let mut known = self.git_known(git_dir)?;
595 595
        let old_heads = self.bookmark_heads()?;
596 596
        let mut new_changes: Vec<Change> = Vec::new();
597 -
        // The state after the most recently imported commit, reused when the
598 -
        // next commit's first parent is that commit.
599 -
        let mut last: Option<(String, State)> = None;
597 +
        // The state after the most recently imported commit, with its Git
598 +
        // tree, reused when the next commit's first parent is that commit.
599 +
        let mut last: Option<(String, State, BTreeMap<String, (String, String)>)> = None;
600 600
        let mut blob_cache: HashMap<String, Vec<u8>> = HashMap::new();
601 601
        let mut cache_bytes: usize = 0;
602 602
…
1 unchanged lines
604 604
            if known.imported.contains_key(&gc.sha) || known.exported.contains_key(&gc.sha) {
605 605
                continue;
606 606
            }
607 -
            // Base: union of every parent's state.
608 -
            let mut base = match (&gc.parents.first(), &last) {
609 -
                (Some(p1), Some((sha, st))) if *p1 == sha => st.clone(),
607 +
            // Base: union of every parent's state. When the first parent is
608 +
            // the commit just imported, its state moves on, and its tree
609 +
            // tells which paths this commit leaves alone.
610 +
            let (mut base, mut prev_tree) = match (gc.parents.first(), last.take()) {
611 +
                (Some(p1), Some((sha, st, tree))) if *p1 == sha => (st, Some(tree)),
610 612
                (Some(p1), _) => {
611 613
                    let mut st = State::new();
612 614
                    self.add_git_ancestors(&mut st, git_dir, &[p1.as_str()], &known)?;
613 -
                    st
615 +
                    (st, None)
614 616
                }
615 -
                (None, _) => State::new(),
617 +
                (None, _) => (State::new(), None),
616 618
            };
617 619
            if gc.parents.len() > 1 {
620 +
                // The other parents bring changes to paths the first
621 +
                // parent's tree cannot vouch for: compare every file.
622 +
                prev_tree = None;
618 623
                report.merges += 1;
619 624
                let mut args: Vec<String> = gc.parents[1..].to_vec();
620 625
                args.push(format!("^{}", gc.parents[0]));
621 626
                let refs: Vec<&str> = args.iter().map(String::as_str).collect();
622 627
                self.add_git_ancestors(&mut base, git_dir, &refs, &known)?;
628 +
            }
629 +
            let tree = list_tree(git_dir, &gc.sha)?;
630 +
            // Text files with the same blob and mode as in the first
631 +
            // parent's tree equal the base's rendering: they are neither
632 +
            // rendered nor read nor compared (spec §12.3, as for a clean
633 +
            // working copy), so a commit costs what it changes.
634 +
            let mut same: HashMap<&str, NodeId> = HashMap::new();
635 +
            if let Some(prev) = &prev_tree {
636 +
                let view = base.tree().view();
637 +
                for (path, v) in &tree {
638 +
                    // Submodule handling reads these two whatever changed.
639 +
                    if prev.get(path) != Some(v) || path == ".gitmodules" || path == ".oxymodules" {
640 +
                        continue;
641 +
                    }
642 +
                    if let Some(e) = view.entries.get(path) {
643 +
                        if e.kind == NodeKind::File && !e.mode.opaque {
644 +
                            same.insert(path.as_str(), e.node);
645 +
                        }
646 +
                    }
647 +
                }
623 648
            }
624 -
            let base_snapshot = base.materialize();
649 +
            let base_snapshot = if same.is_empty() {
650 +
                base.materialize()
651 +
            } else {
652 +
                base.materialize_except(&same.values().copied().collect())
653 +
            };
625 654
626 655
            // The commit's tree as in-memory entries.
627 656
            let mut entries: BTreeMap<String, FsEntry> = BTreeMap::new();
628 657
            let mut gitlinks: Vec<(String, String)> = Vec::new();
629 -
            for (path, (mode, sha)) in list_tree(git_dir, &gc.sha)? {
658 +
            for (path, (mode, sha)) in &tree {
659 +
                let (path, sha) = (path.clone(), sha.clone());
630 660
                let (kind, exec) = match mode.as_str() {
631 661
                    "100644" | "100664" => (NodeKind::File, false),
632 662
                    "100755" => (NodeKind::File, true),
…
4 unchanged lines
637 667
                    }
638 668
                    _ => continue,
639 669
                };
670 +
                if same.contains_key(path.as_str()) {
671 +
                    let mut e = FsEntry::new(kind, exec, Vec::new());
672 +
                    e.unchanged = true;
673 +
                    entries.insert(path, e);
674 +
                    continue;
675 +
                }
640 676
                let content = match blob_cache.get(&sha) {
641 677
                    Some(c) => c.clone(),
642 678
                    None => {
…
114 unchanged lines
757 793
            new_changes.push(change.clone());
758 794
            base.add_unchecked(change);
759 795
            known.imported.insert(gc.sha.clone(), hash);
760 -
            last = Some((gc.sha.clone(), base));
796 +
            last = Some((gc.sha.clone(), base, tree));
761 797
            report.commits += 1;
762 798
        }
763 799
…
380 unchanged lines
modified docs/backlog-overnight-2.md +6 −0
…
114 unchanged lines
115 115
  - Receipts stored in the repository and verified by the hub.
116 116
  - Flow `output` and artifacts, so a release asset links to the receipt that built it.
117 117
- [ ] P4b **Trust dashboard** (item 4): per release and asset, a graph of authors' identities and signatures, the runner and its pinned environment, inputs (state, scope hash), and receipts, with a one-click audit view and export.
118 +
- [x] H11 Run pipeline without a push, made easy (Andy): a primary button on the Flow tab, reasons shown when it is missing, and Run pipeline beside each bookmark and on change pages (runs the change's own state); `POST /flow/runs` takes `{"change"}`.
118 119
- [ ] H10 `oh`, a command-line client for OxygenHub like GitHub's `gh`: a crate `crates/oh` in the oxygenhub workspace on hub-api. First slice: `oh auth login`, `oh issue list|view|create`, `oh proposal list|view|create|land`, `oh run list|view|watch`, `oh release list|create|upload`, with `--json`.
119 120
- [ ] P5 **Web workspaces** (item 2), in parts:
120 121
  - P5a: flow-casfs, a read-only FUSE mount of a state from CAS chunks, measured.
…
609 unchanged lines
730 731
  - **Swimlanes:** confirmed that P3 as built is what he meant.
731 732
  - **Site push token:** remove it from hub.toml. This was not done. The permission check refused my reading even masked token prefixes to confirm our checkouts and runners don't push with it, so Andy is removing it himself.
732 733
- 2026-09-29 **Site push token removed** at Andy's request, after he confirmed no checkout uses it. hub.toml on Earth was backed up to `hub.toml.bak-20260929`. `push_tokens` is now empty and the hub restarted with /health ok. Pushes from oxygen, oxygenhub, oxygenflow and oxygen-vscode still authenticate with their account tokens.
734 +
- 2026-09-29 **H11 done** (oxygenhub#40; Andy's request).
735 +
  - **Andy asked** whether a run without a check-in breaks the history. It does not. Runs only take committed states already in the repository, write no history, and add signed receipts about a state; running it again adds another statement. Only running uncommitted work would be unverifiable, and that stays impossible.
736 +
  - **Run pipeline:** now a primary button on the Flow tab. Viewers who cannot use it are told why: Flow is off, they are not signed in, or they lack write access.
737 +
  - **Bookmarks and changes:** each bookmark has Run pipeline beside it. On a change page it runs the state whose only head is that change; such runs carry no bookmark and are labelled "change". `POST /flow/runs` accepts `{"change"}`, and the empty-state text mentions manual runs.
738 +
  - **Tests and deploy:** the manual-run test covers a change run, an unknown change, the reasons text and the missing buttons. Deployed; the hub run is green.