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. |