1409 lines · 51.5 KB · Rust
RawDownload
// Copyright 2026 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::collections::HashMap; use assert_matches::assert_matches;use jj_lib::backend::CommitId;use jj_lib::commit::Commit;use jj_lib::git;use jj_lib::git::GitImportOptions;use jj_lib::git::GitImportRefUpdate;use jj_lib::git::GitImportStats;use jj_lib::git_sync::GitSyncError;use jj_lib::git_sync::GitSyncUnsupportedReason;use jj_lib::git_sync::sync_imported_refs;use jj_lib::object_id::ObjectId as _;use jj_lib::op_store::RefTarget;use jj_lib::op_store::RemoteRef;use jj_lib::op_store::RemoteRefState;use jj_lib::ref_name::RemoteRefSymbol;use jj_lib::ref_name::WorkspaceName;use jj_lib::repo::MutableRepo;use jj_lib::repo::Repo as _;use jj_lib::rewrite::EmptyBehavior;use jj_lib::rewrite::RebaseOptions;use jj_lib::rewrite::RebasedCommit;use maplit::hashset;use pollster::FutureExt as _;use testutils::CommitBuilderExt as _;use testutils::TestRepo;use testutils::TestRepoBackend;use testutils::TestResult;use testutils::create_random_commit;use testutils::write_random_commit;use testutils::write_random_commit_with_parents; fn imported_update(name: &str, remote: &str, old: &Commit, new: &Commit) -> GitImportRefUpdate {    GitImportRefUpdate::new(        RemoteRefSymbol {            name: name.as_ref(),            remote: remote.as_ref(),        }        .to_owned(),        RemoteRef {            target: RefTarget::normal(old.id().clone()),            state: RemoteRefState::Tracked,        },        RefTarget::normal(new.id().clone()),    )} fn apply_import_updates(    mut_repo: &mut MutableRepo,    mut updates: Vec<GitImportRefUpdate>,) -> GitImportStats {    updates.sort_unstable_by(|left, right| left.symbol.cmp(&right.symbol));    for update in &updates {        mut_repo.set_remote_bookmark(            update.symbol.as_ref(),            RemoteRef {                target: update.new_target.clone(),                state: RemoteRefState::Tracked,            },        );    }    GitImportStats {        changed_remote_bookmarks: updates,        ..Default::default()    }} fn expect_rewritten<'a>(results: &'a HashMap<CommitId, RebasedCommit>, old: &Commit) -> &'a Commit {    match results.get(old.id()) {        Some(RebasedCommit::Rewritten(new)) => new,        Some(RebasedCommit::Abandoned { parent_id }) => {            panic!(                "expected commit {} to be rewritten, but it was abandoned onto {parent_id}",                old.id()            );        }        None => panic!("expected a result for commit {}", old.id()),    }} fn set_git_ref(git_repo: &gix::Repository, name: &str, target: &Commit) -> TestResult {    let target = gix::ObjectId::from_bytes_or_panic(target.id().as_bytes());    git_repo.reference(name, target, gix::refs::transaction::PreviousValue::Any, "")?;    Ok(())} #[test]fn test_sync_real_imported_fast_forward() -> TestResult {    // main@origin is first imported at old_main and marked tracked. The feature    // commit is selected before main@origin advances to new_main. The second import    // returns that fast-forward to sync_imported_refs().    //    // Before import and sync:    //    // old_main ─┬─> new_main (new main@origin target)    //           └─> feature (selected)    //    // After sync:    //    // old_main ──> new_main (main@origin) ──> feature'    let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git);    let git_repo = git::get_git_repo(test_repo.repo.store())?;    let import_options = GitImportOptions {        abandon_unreachable_commits: true,        record_synthetic_predecessors: true,        remote_auto_track_bookmarks: HashMap::new(),    };     let mut tx = test_repo.repo.start_transaction();    let old_main = write_random_commit(tx.repo_mut());    set_git_ref(&git_repo, "refs/remotes/origin/main", &old_main)?;    git::import_refs(tx.repo_mut(), &import_options).block_on()?;    tx.repo_mut().set_remote_bookmark(        RemoteRefSymbol {            name: "main".as_ref(),            remote: "origin".as_ref(),        },        RemoteRef {            target: RefTarget::normal(old_main.id().clone()),            state: RemoteRefState::Tracked,        },    );    let new_main = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let feature = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let selected_feature_id = feature.id().clone();    let repo = tx.commit("setup").block_on()?;     set_git_ref(&git_repo, "refs/remotes/origin/main", &new_main)?;    let mut tx = repo.start_transaction();    let stats = git::import_refs(tx.repo_mut(), &import_options).block_on()?;    assert!(!tx.repo().has_rewrites());    let result = sync_imported_refs(        tx.repo_mut(),        &stats,        vec![selected_feature_id],        &RebaseOptions::default(),    )    .block_on()?;     let new_feature = expect_rewritten(&result, &feature);    assert_eq!(new_feature.parent_ids(), vec![new_main.id().clone()]);    assert!(!tx.repo().has_rewrites());    Ok(())} #[test]fn test_sync_exact_selected_graph_and_references() -> TestResult {    // Imported: main@origin moved from old_main to new_main.    // Selected: feature_auth, feature_docs, release_merge.    //    // Before sync:    //    // old_main ─┬─> feature_auth ───────────────┐    //           ├─> feature_docs ───────────────┼─> release_merge (@, release)    //           └─> new_main (main@origin) ─────┘    //    // After sync:    //    // old_main ──> new_main (main@origin)    //              ├─> feature_auth' ──────────┐    //              ├─> feature_docs' ──────────┼─> release_merge' (@, release)    //              └───────────────────────────┘    let test_repo = TestRepo::init();    let mut tx = test_repo.repo.start_transaction();    let old_main = write_random_commit(tx.repo_mut());    let new_main = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let feature_auth = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let feature_docs = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let release_merge =        write_random_commit_with_parents(tx.repo_mut(), &[&feature_auth, &feature_docs, &new_main]);    let symbol = RemoteRefSymbol {        name: "main".as_ref(),        remote: "origin".as_ref(),    };    tx.repo_mut().set_local_bookmark_target(        "release".as_ref(),        RefTarget::normal(release_merge.id().clone()),    );    tx.repo_mut().set_wc_commit(        WorkspaceName::DEFAULT.to_owned(),        release_merge.id().clone(),    )?;    let stats = apply_import_updates(        tx.repo_mut(),        vec![imported_update("main", "origin", &old_main, &new_main)],    );    let options = RebaseOptions {        // Rewrite roots may use merge simplification, but release_merge is a        // structural descendant and must retain its explicit new_main parent.        simplify_ancestor_merge: true,        ..Default::default()    };    let result = sync_imported_refs(        tx.repo_mut(),        &stats,        vec![            feature_auth.id().clone(),            feature_docs.id().clone(),            release_merge.id().clone(),        ],        &options,    )    .block_on()?;     assert_eq!(result.len(), 3);    let new_feature_auth = expect_rewritten(&result, &feature_auth);    let new_feature_docs = expect_rewritten(&result, &feature_docs);    let new_release_merge = expect_rewritten(&result, &release_merge);    assert_eq!(new_feature_auth.parent_ids(), vec![new_main.id().clone()]);    assert_eq!(new_feature_docs.parent_ids(), vec![new_main.id().clone()]);    assert_eq!(        new_release_merge.parent_ids(),        vec![            new_feature_auth.id().clone(),            new_feature_docs.id().clone(),            new_main.id().clone(),        ]    );    assert_eq!(        tx.repo().get_local_bookmark("release".as_ref()),        RefTarget::normal(new_release_merge.id().clone())    );    assert_eq!(        tx.repo().view().get_wc_commit_id(WorkspaceName::DEFAULT),        Some(new_release_merge.id())    );    assert_eq!(        tx.repo().get_remote_bookmark(symbol).target,        RefTarget::normal(new_main.id().clone())    );    assert_eq!(        *tx.repo().view().heads(),        hashset! { new_release_merge.id().clone() }    );    assert!(!tx.repo().has_rewrites());    Ok(())} #[test]fn test_sync_empty_wc_root_with_hidden_selected_descendant() -> TestResult {    // Imported: main@origin moved from old_main to new_main.    // Selected: release_base and feature_hidden, which is not a repository head.    //    // Before sync:    //    // old_main ─┬─> release_base (@, empty) ──> feature_hidden (hidden)    //           └─> new_main (main@origin)    //    // After sync (release_base abandoned):    //    // old_main ──> new_main (main@origin)    //              ├─> feature_hidden'    //              └─> new_wc (@, empty)    let test_repo = TestRepo::init();    let mut tx = test_repo.repo.start_transaction();    let old_main = write_random_commit(tx.repo_mut());    let new_main = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let release_base = create_random_commit(tx.repo_mut())        .set_parents(vec![old_main.id().clone()])        .set_tree(old_main.tree())        .write_unwrap();    let feature_hidden = write_random_commit_with_parents(tx.repo_mut(), &[&release_base]);    tx.repo_mut().remove_head(feature_hidden.id());    tx.repo_mut()        .set_wc_commit(WorkspaceName::DEFAULT.to_owned(), release_base.id().clone())?;    let stats = apply_import_updates(        tx.repo_mut(),        vec![imported_update("main", "origin", &old_main, &new_main)],    );    let result = sync_imported_refs(        tx.repo_mut(),        &stats,        vec![release_base.id().clone(), feature_hidden.id().clone()],        &RebaseOptions {            empty: EmptyBehavior::AbandonAllEmpty,            ..Default::default()        },    )    .block_on()?;     assert_matches!(        &result[release_base.id()],        RebasedCommit::Abandoned { parent_id } if parent_id == new_main.id()    );    let new_feature_hidden = expect_rewritten(&result, &feature_hidden);    assert_eq!(new_feature_hidden.parent_ids(), vec![new_main.id().clone()]);    let wc_id = tx        .repo()        .view()        .get_wc_commit_id(WorkspaceName::DEFAULT)        .expect("working copy should be recreated after abandoning its selected commit");    let wc = tx.repo().store().get_commit(wc_id)?;    assert_eq!(wc.parent_ids(), vec![new_main.id().clone()]);    assert_eq!(        *tx.repo().view().heads(),        hashset! { new_feature_hidden.id().clone(), wc.id().clone() }    );    assert!(!tx.repo().has_rewrites());    Ok(())} #[test]fn test_sync_keeps_empty_structural_descendant() -> TestResult {    // Imported: main@origin moved from old_main to new_main.    // Selected: feature_root and empty_descendant.    //    // Before sync:    //    // old_main ─┬─> new_main (main@origin)    //           └─> feature_root ──> empty_descendant (empty)    //    // After sync:    //    // old_main ──> new_main (main@origin)    //              └─> feature_root' ──> empty_descendant' (empty)    //    // Caller empty-commit policy applies to the rewrite root, while structural    // descendants are preserved one-for-one.    let test_repo = TestRepo::init();    let mut tx = test_repo.repo.start_transaction();    let old_main = write_random_commit(tx.repo_mut());    let new_main = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let feature_root = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let empty_descendant = create_random_commit(tx.repo_mut())        .set_parents(vec![feature_root.id().clone()])        .set_tree(feature_root.tree())        .write_unwrap();    let stats = apply_import_updates(        tx.repo_mut(),        vec![imported_update("main", "origin", &old_main, &new_main)],    );     let result = sync_imported_refs(        tx.repo_mut(),        &stats,        vec![feature_root.id().clone(), empty_descendant.id().clone()],        &RebaseOptions {            empty: EmptyBehavior::AbandonAllEmpty,            ..Default::default()        },    )    .block_on()?;     assert_eq!(result.len(), 2);    let new_feature_root = expect_rewritten(&result, &feature_root);    let new_empty_descendant = expect_rewritten(&result, &empty_descendant);    assert_eq!(new_feature_root.parent_ids(), vec![new_main.id().clone()]);    assert_eq!(        new_empty_descendant.parent_ids(),        vec![new_feature_root.id().clone()]    );    Ok(())} #[test]fn test_sync_ignores_selected_commits_on_imported_lineage() -> TestResult {    // Imported: main@origin moved from old_main to new_main through midpoint.    // Selected: midpoint, deployed, and feature_root.    //    // Before sync:    //    // old_main ──> midpoint ─┬─> new_main (main@origin) ──> deployed    //                      └─> feature_root    //    // After sync: only the side-branch feature is rewritten. Selected commits    // on or beyond the imported lineage are not part of the rewrite plan.    //    // old_main ──> midpoint ──> new_main (main@origin) ─┬─> deployed    //                                               └─> feature_root'    let test_repo = TestRepo::init();    let mut tx = test_repo.repo.start_transaction();    let old_main = write_random_commit(tx.repo_mut());    let midpoint = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let new_main = write_random_commit_with_parents(tx.repo_mut(), &[&midpoint]);    let deployed = write_random_commit_with_parents(tx.repo_mut(), &[&new_main]);    let feature_root = write_random_commit_with_parents(tx.repo_mut(), &[&midpoint]);    let stats = apply_import_updates(        tx.repo_mut(),        vec![imported_update("main", "origin", &old_main, &new_main)],    );     let result = sync_imported_refs(        tx.repo_mut(),        &stats,        vec![            midpoint.id().clone(),            deployed.id().clone(),            feature_root.id().clone(),        ],        &RebaseOptions::default(),    )    .block_on()?;     assert_eq!(result.len(), 1);    assert!(!result.contains_key(midpoint.id()));    assert!(!result.contains_key(deployed.id()));    let new_feature_root = expect_rewritten(&result, &feature_root);    assert_eq!(new_feature_root.parent_ids(), vec![new_main.id().clone()]);    Ok(())} #[test]fn test_sync_rejects_conflicting_rewrite_roles_across_moves() -> TestResult {    // Imported: main@origin moved old_main to new_main through old_release;    // release@origin moved old_release to new_release through feature_root.    // Selected: feature_root and empty_mixed_role.    //    // Before sync:    //    // old_main ──> old_release ─┬─> new_main (main@origin)    //                           └─> feature_root ─┬─> new_release (release@origin)    //                                              └─> empty_mixed_role    //    // The selected child is structural for the main update and a rewrite root    // for the release update. Applying the release parent override would    // detach it from the rewritten selected parent produced by the main    // update. The planner has no branch-ownership input, so it rejects the    // combined plan instead of choosing one update implicitly.    let test_repo = TestRepo::init();    let mut tx = test_repo.repo.start_transaction();    let old_main = write_random_commit(tx.repo_mut());    let old_release = write_random_commit_with_parents(tx.repo_mut(), &[&old_main]);    let new_main = write_random_commit_with_parents(tx.repo_mut(), &[&old_release]);    let feature_root = write_random_commit_with_parents(tx.repo_mut(), &[&old_release]);    let new_release = create_random_commit(tx.repo_mut())        .set_parents(vec![feature_root.id().clone()])        .set_tree(feature_root.tree())        .write_unwrap();    let empty_mixed_role = create_random_commit(tx.repo_mut())        .set_parents(vec![feature_root.id().clone()])        .set_tree(feature_root.tree())        .write_unwrap();    let stats = apply_import_updates(        tx.repo_mut(),        vec![            imported_update("main", "origin", &old_main, &new_main),            imported_update("release", "origin", &old_release, &new_release),        ],    );     let error = sync_imported_refs(        tx.repo_mut(),        &stats,        vec![feature_root.id().clone(), empty_mixed_role.id().clone()],        &RebaseOptions {            empty: EmptyBehavior::AbandonAllEmpty,            ..Default::default()        },    )    .block_on()    .unwrap_err();    assert_matches!(        error,        GitSyncError::ConflictingRewriteRoles { commit_ids }            if commit_ids == vec![empty_mixed_role.id().clone()]    );    Ok(())} #[test]fn test_sync_rejects_merge_with_conflicting_rewrite_roles() -> TestResult {    // Imported: main@origin moved old to new; release@origin moved new to newer.    // Selected: local and merge.    //    // old ─┬─> new ──> newer (release@origin)    //      │    └────────────┐    //      └─> local ────────┴─> merge    //    // merge is structural for the main update because it follows local, but    // it is a rewrite root for the release update through new. Reject rather    // than deciding whether to treat it as a root or a descendant.    let test_repo = TestRepo::init();    let mut tx = test_repo.repo.start_transaction();    let old = write_random_commit(tx.repo_mut());    let new = write_random_commit_with_parents(tx.repo_mut(), &[&old]);    let newer = write_random_commit_with_parents(tx.repo_mut(), &[&new]);    let local = write_random_commit_with_parents(tx.repo_mut(), &[&old]);    let merge = write_random_commit_with_parents(tx.repo_mut(), &[&new, &local]);    let stats = apply_import_updates(        tx.repo_mut(),        vec![            imported_update("main", "origin", &old, &new),            imported_update("release", "origin", &new, &newer),        ],    );

Lines 1–500 of 1409.