OXYGENHUB

Say which bookmark pull pulled, and which are left behind 7d9dab131937

pull acts on the current bookmark. On rstar-api, pulls run while on a
feature bookmark said only "Already up to date" while main, which the
fetch had just found one behind, was what the user meant; the next pull
after switching took it. pull now names the bookmark it pulled and each
other bookmark behind its remote, with the command that takes it; --json
carries the bookmark. A core test covers a remote main moving to
changes already here, as a hub landing does.

closes #29
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-29 21:31 UTC
Signature
✔ signed by Andy McCall
Hash
7d9dab131937b35c1c03543ee2346d3821d80c879936bbcb8666f65ec466a9c4
Depends on
4fe7d6045860 6f8aeb996558 75167672ebcd 982864068a59 c6aa5bd9d1c8 fce44ae9ad29
Depended on by
d36ba2f79f95 767ac6cd0545 f6dccc4533c8 14edbaaf5291 da868d5d2646 86e1824f2d0b
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
8 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/tests.rs +44 −0
…
5665 unchanged lines
5666 5666
    assert_eq!(s.wc.deps, repo.bookmark_heads().unwrap());
5667 5667
    assert!(root.join("many/f299.txt").exists());
5668 5668
}
5669 +
5670 +
/// oxygen#29 (metalgear, rstar-api): after a proposal landed on the hub,
5671 +
/// moving `main` to a change this clone had pushed on another bookmark,
5672 +
/// the first `pull` said "Already up to date" while `main` was behind.
5673 +
#[test]
5674 +
fn pull_takes_a_remote_move_to_changes_already_here() {
5675 +
    hermetic();
5676 +
    let dir = tempfile::tempdir().unwrap();
5677 +
    let root = dir.path();
5678 +
    let origin_path = root.join("origin");
5679 +
    let origin = Repo::init(&origin_path).unwrap();
5680 +
    let url = origin_path.to_string_lossy().to_string();
5681 +
    let mut b = Repo::clone(&url, &root.join("b")).unwrap();
5682 +
    write(&root.join("b"), "base.txt", b"base\n");
5683 +
    b.commit("base", &[]).unwrap();
5684 +
    assert!(b.push("origin", false).unwrap()[0].ok);
5685 +
5686 +
    // A feature bookmark with one change, pushed.
5687 +
    b.bookmark_create("feat", None).unwrap();
5688 +
    b.bookmark_switch("feat", false).unwrap();
5689 +
    write(&root.join("b"), "x.txt", b"x\n");
5690 +
    let x = b.commit("x", &[]).unwrap();
5691 +
    assert!(b.push("origin", false).unwrap()[0].ok);
5692 +
    b.bookmark_switch("main", false).unwrap();
5693 +
5694 +
    // The remote lands it: its main moves to the feature's state.
5695 +
    let feat = origin
5696 +
        .store()
5697 +
        .get_state(&origin.view().unwrap().bookmarks["feat"])
5698 +
        .unwrap()
5699 +
        .heads;
5700 +
    origin.bookmark_set("main", &feat).unwrap();
5701 +
5702 +
    let r = b.pull("origin").unwrap();
5703 +
    assert!(r.changed, "{r:?}");
5704 +
    assert!(r.heads.contains(&x) || b.load_state(&r.heads).unwrap().contains(&x));
5705 +
    let main = b
5706 +
        .bookmarks()
5707 +
        .unwrap()
5708 +
        .into_iter()
5709 +
        .find(|m| m.name == "main")
5710 +
        .unwrap();
5711 +
    assert_eq!(main.behind, Some(0), "{main:?}");
5712 +
}
modified src/cli/sync.rs +36 −1
…
194 unchanged lines
195 195
    Ok(0)
196 196
}
197 197
198 +
/// Pull acts on the current bookmark only: name the others the fetch
199 +
/// found behind their remote, which is easy to take for the current one
200 +
/// (oxygen#29).
201 +
fn others_behind(repo: &oxy_core::repo::Repo, remote: &str) {
202 +
    let Ok(bookmarks) = repo.bookmarks() else {
203 +
        return;
204 +
    };
205 +
    for b in bookmarks {
206 +
        let Some(n) = b.behind.filter(|n| *n > 0) else {
207 +
            continue;
208 +
        };
209 +
        if b.current {
210 +
            continue;
211 +
        }
212 +
        println!(
213 +
            "  {} {}",
214 +
            ui::note(),
215 +
            ui::dim(format!(
216 +
                "{} is {} behind {remote}/{}: oxy bookmark switch {} then oxy pull to take {}",
217 +
                b.name,
218 +
                plural(n, "change"),
219 +
                b.name,
220 +
                b.name,
221 +
                if n == 1 { "it" } else { "them" }
222 +
            ))
223 +
        );
224 +
    }
225 +
}
226 +
198 227
pub fn pull(remote: Option<&str>, as_json: bool) -> Result<i32> {
199 228
    let repo = open()?;
200 229
    let remote = remote_name(remote);
…
9 unchanged lines
210 239
            "{}",
211 240
            json::obj([
212 241
                ("remote", json::s(&remote)),
242 +
                ("bookmark", json::s(&repo.config.bookmark)),
213 243
                ("heads", json::hashes(&r.heads)),
214 244
                ("changed", json::b(r.changed)),
215 245
                ("checked_out", json::b(r.checked_out)),
…
6 unchanged lines
222 252
        sp.done(&format!(
223 253
            "{} {}",
224 254
            ui::ok(ui::glyph("✔", "*")),
225 -
            ui::dim("Already up to date.")
255 +
            ui::dim(format!(
256 +
                "{} is up to date with {remote}.",
257 +
                repo.config.bookmark
258 +
            ))
226 259
        ));
260 +
        others_behind(&repo, &remote);
227 261
        return Ok(0);
228 262
    }
229 263
    sp.done(&format!(
…
15 unchanged lines
245 279
            ui::warn("the result has conflicts; see oxy status")
246 280
        );
247 281
    }
282 +
    others_behind(&repo, &remote);
248 283
    Ok(0)
249 284
}
250 285
…
462 unchanged lines
modified tests/cli_json.rs +42 −0
…
445 unchanged lines
446 446
    assert!(out.contains("only lacks 2 changes that main has"), "{out}");
447 447
    assert!(!r.join("a.txt").exists() && r.join("b.txt").exists());
448 448
}
449 +
450 +
/// oxygen#29: pull acts on the current bookmark; a pull on a feature
451 +
/// bookmark said only "Already up to date" while main was behind.
452 +
#[test]
453 +
fn pull_names_its_bookmark_and_the_others_left_behind() {
454 +
    let tmp = tempfile::tempdir().unwrap();
455 +
    let home = tmp.path().join("home");
456 +
    std::fs::create_dir_all(&home).unwrap();
457 +
    let origin = tmp.path().join("origin");
458 +
    assert_eq!(
459 +
        oxy(tmp.path(), &home, &["init", origin.to_str().unwrap()]).0,
460 +
        0
461 +
    );
462 +
    let (a, b) = (tmp.path().join("a"), tmp.path().join("b"));
463 +
    for d in [&a, &b] {
464 +
        assert_eq!(
465 +
            oxy(
466 +
                tmp.path(),
467 +
                &home,
468 +
                &["clone", origin.to_str().unwrap(), d.to_str().unwrap()]
469 +
            )
470 +
            .0,
471 +
            0
472 +
        );
473 +
    }
474 +
    write(&a, "one.txt", "one\n");
475 +
    assert_eq!(oxy(&a, &home, &["commit", "-m", "one"]).0, 0);
476 +
    assert_eq!(oxy(&a, &home, &["push"]).0, 0);
477 +
    assert_eq!(oxy(&b, &home, &["pull"]).0, 0);
478 +
    assert_eq!(oxy(&b, &home, &["bookmark", "create", "feat"]).0, 0);
479 +
    assert_eq!(oxy(&b, &home, &["bookmark", "switch", "feat"]).0, 0);
480 +
    assert_eq!(oxy(&b, &home, &["push", "-b", "feat"]).0, 0);
481 +
    write(&a, "two.txt", "two\n");
482 +
    assert_eq!(oxy(&a, &home, &["commit", "-m", "two"]).0, 0);
483 +
    assert_eq!(oxy(&a, &home, &["push"]).0, 0);
484 +
485 +
    let (code, out, err) = oxy(&b, &home, &["pull"]);
486 +
    assert_eq!(code, 0, "{err}");
487 +
    let out = String::from_utf8_lossy(&out);
488 +
    assert!(out.contains("feat is up to date with origin"), "{out}");
489 +
    assert!(out.contains("main is 1 change behind origin/main"), "{out}");
490 +
}