GPT 5.5reviewed998d14 months agoHistory
1012 lines · 36.9 KB · Rust
RawDownload
pub fn restricted() -> crate::open::Options {    crate::open::Options::isolated().config_overrides(["user.name=gitoxide", "user.email=gitoxide@localhost"])} /// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_.fn hex_to_id(hex: &str) -> gix_hash::ObjectId {    gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("valid hex object id")} mod update {    use gix_testtools::Result;     use super::hex_to_id;    use crate as gix;     fn base_repo_path() -> String {        gix::path::realpath(            gix_testtools::scripted_fixture_read_only("make_remote_repos.sh")                .unwrap()                .join("base"),        )        .unwrap()        .to_string_lossy()        .into_owned()    }     fn repo(name: &str) -> gix::Repository {        let dir = gix_testtools::scripted_fixture_read_only_with_args_single_archive(            "make_fetch_repos.sh",            [base_repo_path()],        )        .unwrap();        gix::open_opts(dir.join(name), restricted()).unwrap()    }    fn named_repo(name: &str) -> gix::Repository {        let dir = gix_testtools::scripted_fixture_read_only("make_remote_repos.sh").unwrap();        gix::open_opts(dir.join(name), restricted()).unwrap()    }    fn repo_rw(name: &str) -> (gix::Repository, gix_testtools::tempfile::TempDir) {        let dir = gix_testtools::scripted_fixture_writable_with_args_single_archive(            "make_fetch_repos.sh",            [base_repo_path()],            gix_testtools::Creation::Execute,        )        .unwrap();        let repo = gix::open_opts(dir.path().join(name), restricted()).unwrap();        (repo, dir)    }    /// Resolve `name` to its peeled object id, so expected ids track the fixture's hash.    fn peeled_id(repo: &gix::Repository, name: &str) -> gix_hash::ObjectId {        repo.find_reference(name)            .expect("reference exists")            .peel_to_id()            .expect("ref peels to an object")            .detach()    }    use gix_ref::{        Target, TargetRef,        transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},    };     use crate::{        bstr::BString,        remote::{            fetch,            fetch::{                RefLogMessage,                refmap::{Mapping, Source, SpecIndex},                refs::{tests::restricted, update::TypeChange},            },        },    };     #[test]    fn various_valid_updates() {        let repo = repo("two-origins");        for (spec, expected_mode, reflog_message, detail) in [            (                "refs/heads/main:refs/remotes/origin/main",                fetch::refs::update::Mode::NoChangeNeeded,                Some("no update will be performed"),                "these refs are en-par since the initial clone",            ),            (                "refs/heads/main",                fetch::refs::update::Mode::NoChangeNeeded,                None,                "without local destination ref there is nothing to do for us, ever (except for FETCH_HEADs) later",            ),            (                "refs/heads/main:refs/remotes/origin/new-main",                fetch::refs::update::Mode::New,                Some("storing ref"),                "the destination branch doesn't exist and needs to be created",            ),            (                "refs/heads/main:refs/heads/feature",                fetch::refs::update::Mode::New,                Some("storing head"),                "reflog messages are specific to the type of branch stored, to some limited extend",            ),            (                "refs/heads/main:refs/tags/new-tag",                fetch::refs::update::Mode::New,                Some("storing tag"),                "reflog messages are specific to the type of branch stored, to some limited extend",            ),            (                "+refs/heads/main:refs/remotes/origin/new-main",                fetch::refs::update::Mode::New,                Some("storing ref"),                "just to validate that we really are in dry-run mode, or else this ref would be present now",            ),            (                "+refs/heads/main:refs/remotes/origin/g",                fetch::refs::update::Mode::FastForward,                Some("fast-forward (guessed in dry-run)"),                "a forced non-fastforward (main goes backwards), but dry-run calls it fast-forward",            ),            (                "+refs/heads/main:refs/tags/b-tag",                fetch::refs::update::Mode::Forced,                Some("updating tag"),                "tags can only be forced",            ),            (                "refs/heads/main:refs/tags/b-tag",                fetch::refs::update::Mode::RejectedTagUpdate,                None,                "otherwise a tag is always refusing itself to be overwritten (no-clobber)",            ),            (                "+refs/remotes/origin/g:refs/heads/main",                fetch::refs::update::Mode::RejectedCurrentlyCheckedOut {                    worktree_dirs: vec![repo.workdir().expect("present").to_owned()],                },                None,                "checked out branches cannot be written, as it requires a merge of sorts which isn't done here",            ),            (                "refs/remotes/origin/g:refs/heads/not-currently-checked-out",                fetch::refs::update::Mode::FastForward,                Some("fast-forward (guessed in dry-run)"),                "a fast-forward only fast-forward situation, all good",            ),        ] {            let (mapping, specs) = mapping_from_spec(spec, &repo);            let out = fetch::refs::update(                &repo,                prefixed("action"),                &mapping,                &specs,                &[],                fetch::Tags::None,                reflog_message.map_or(fetch::DryRun::No, |_| fetch::DryRun::Yes),                fetch::WritePackedRefs::Never,            )            .unwrap();             assert_eq!(                out.updates,                vec![fetch::refs::Update {                    type_change: None,                    mode: expected_mode.clone(),                    edit_index: reflog_message.map(|_| 0),                }],                "{spec:?}: {detail}"            );            assert_eq!(out.edits.len(), reflog_message.map_or(0, |_| 1));            if let Some(reflog_message) = reflog_message {                let edit = &out.edits[0];                match &edit.change {                    Change::Update { log, new, .. } => {                        assert_eq!(                            log.message,                            format!("action: {reflog_message}"),                            "{spec}: reflog messages are specific and we emulate git word for word"                        );                        let remote_ref = repo                            .find_reference(specs[0].to_ref().source().expect("always present"))                            .unwrap();                        assert_eq!(                            new.id(),                            remote_ref.target().id(),                            "remote ref provides the id to set in the local reference"                        );                    }                    _ => unreachable!("only updates"),                }            }        }         let missing = "f".repeat(repo.object_hash().len_in_hex());        let (mapping, specs) = mapping_from_spec(&format!("{missing}:refs/heads/invalid-source-object"), &repo);        let out = fetch::refs::update(            &repo,            prefixed("action"),            &mapping,            &specs,            &[],            fetch::Tags::None,            fetch::DryRun::No,            fetch::WritePackedRefs::Never,        )        .unwrap();        assert_eq!(            out.updates,            vec![fetch::refs::Update {                type_change: None,                mode: fetch::refs::update::Mode::RejectedSourceObjectNotFound {                    id: hex_to_id(&missing),                },                edit_index: None,            }],            "a source object that isn't present is rejected"        );        assert_eq!(out.edits.len(), 0);    }     #[test]    fn checked_out_branches_in_worktrees_are_rejected_with_additional_information() -> Result {        let root = gix_path::realpath(gix_testtools::scripted_fixture_read_only_with_args_single_archive(            "make_fetch_repos.sh",            [base_repo_path()],        )?)?;        let repo = root.join("worktree-root");        let repo = gix::open_opts(repo, restricted())?;        for (branch, path_from_root) in [            ("main", "worktree-root"),            ("wt-a-nested", "prev/wt-a-nested"),            ("wt-a", "wt-a"),            ("nested-wt-b", "wt-a/nested-wt-b"),            ("wt-c-locked", "wt-c-locked"),            ("wt-deleted", "wt-deleted"),        ] {            let spec = format!("refs/heads/main:refs/heads/{branch}");            let (mappings, specs) = mapping_from_spec(&spec, &repo);            let out = fetch::refs::update(                &repo,                prefixed("action"),                &mappings,                &specs,                &[],                fetch::Tags::None,                fetch::DryRun::Yes,                fetch::WritePackedRefs::Never,            )?;             assert_eq!(                out.updates,                vec![fetch::refs::Update {                    mode: fetch::refs::update::Mode::RejectedCurrentlyCheckedOut {                        worktree_dirs: vec![root.join(path_from_root)],                    },                    type_change: None,                    edit_index: None,                }],                "{spec}: checked-out checks are done before checking if a change would actually be required (here it isn't)"            );            assert_eq!(out.edits.len(), 0);        }        Ok(())    }     #[test]    fn unborn_remote_branches_can_be_created_locally_if_they_are_new() -> Result {        let repo = named_repo("unborn");        let (mappings, specs) = mapping_from_spec("HEAD:refs/remotes/origin/HEAD", &repo);        assert_eq!(mappings.len(), 1);        let out = fetch::refs::update(            &repo,            prefixed("action"),            &mappings,            &specs,            &[],            fetch::Tags::None,            fetch::DryRun::Yes,            fetch::WritePackedRefs::Never,        )?;        assert_eq!(            out.updates,            vec![fetch::refs::Update {                mode: fetch::refs::update::Mode::New,                type_change: None,                edit_index: Some(0)            }]        );        assert_eq!(out.edits.len(), 1, "we are OK with creating unborn refs");        Ok(())    }     #[test]    fn unborn_remote_branches_can_update_local_unborn_branches() -> Result {        let repo = named_repo("unborn");        let (mappings, specs) = mapping_from_spec("HEAD:refs/heads/existing-unborn-symbolic", &repo);        assert_eq!(mappings.len(), 1);        let out = fetch::refs::update(            &repo,            prefixed("action"),            &mappings,            &specs,            &[],            fetch::Tags::None,            fetch::DryRun::Yes,            fetch::WritePackedRefs::Never,        )?;        assert_eq!(            out.updates,            vec![fetch::refs::Update {                mode: fetch::refs::update::Mode::NoChangeNeeded,                type_change: None,                edit_index: Some(0)            }]        );        assert_eq!(out.edits.len(), 1, "we are OK with updating unborn refs");        assert_eq!(            out.edits[0],            RefEdit {                change: Change::Update {                    log: LogChange {                        mode: RefLog::AndReference,                        force_create_reflog: false,                        message: "action: change unborn ref".into(),                    },                    expected: PreviousValue::MustExistAndMatch(Target::Symbolic(                        "refs/heads/main".try_into().expect("valid"),                    )),                    new: Target::Symbolic("refs/heads/main".try_into().expect("valid")),                },                name: "refs/heads/existing-unborn-symbolic".try_into().expect("valid"),                deref: false,            }        );         let (mappings, specs) = mapping_from_spec("HEAD:refs/heads/existing-unborn-symbolic-other", &repo);        assert_eq!(mappings.len(), 1);        let out = fetch::refs::update(            &repo,            prefixed("action"),            &mappings,            &specs,            &[],            fetch::Tags::None,            fetch::DryRun::Yes,            fetch::WritePackedRefs::Never,        )?;        assert_eq!(            out.updates,            vec![fetch::refs::Update {                mode: fetch::refs::update::Mode::Forced,                type_change: None,                edit_index: Some(0)            }]        );        assert_eq!(            out.edits.len(),            1,            "we are OK with creating unborn refs even without actually forcing it"        );        assert_eq!(            out.edits[0],            RefEdit {                change: Change::Update {                    log: LogChange {                        mode: RefLog::AndReference,                        force_create_reflog: false,                        message: "action: change unborn ref".into(),                    },                    expected: PreviousValue::MustExistAndMatch(Target::Symbolic(                        "refs/heads/other".try_into().expect("valid"),                    )),                    new: Target::Symbolic("refs/heads/main".try_into().expect("valid")),                },                name: "refs/heads/existing-unborn-symbolic-other".try_into().expect("valid"),                deref: false,            }        );        Ok(())    }     #[test]    fn remote_symbolic_refs_with_locally_unavailable_target_result_in_valid_peeled_branches() -> Result {        let remote_repo = named_repo("one-commit-with-symref");        let local_repo = named_repo("unborn");        let (mappings, specs) = mapping_from_spec("refs/heads/symbolic:refs/heads/new", &remote_repo);        assert_eq!(mappings.len(), 1);         let out = fetch::refs::update(            &local_repo,            prefixed("action"),            &mappings,            &specs,            &[],            fetch::Tags::None,            fetch::DryRun::Yes,            fetch::WritePackedRefs::Never,        )?;        assert_eq!(            out.updates,            vec![fetch::refs::Update {                mode: fetch::refs::update::Mode::New,                type_change: None,                edit_index: Some(0)            }]        );        assert_eq!(out.edits.len(), 1);        let target = Target::Object(peeled_id(&remote_repo, "refs/heads/symbolic"));        assert_eq!(            out.edits[0],            RefEdit {                change: Change::Update {                    log: LogChange {                        mode: RefLog::AndReference,                        force_create_reflog: false,                        message: "action: storing head".into(),                    },                    expected: PreviousValue::ExistingMustMatch(target.clone()),                    new: target,                },                name: "refs/heads/new".try_into().expect("valid"),                deref: false,            },            "we create local-refs whose targets aren't present yet, even though the remote knows them.\             This leaves the caller with assuring all refs are mentioned in mappings."        );        Ok(())    }     #[test]    fn remote_symbolic_refs_with_locally_unavailable_target_dont_overwrite_valid_local_branches() -> Result {        let remote_repo = named_repo("one-commit-with-symref");        let local_repo = named_repo("one-commit-with-symref-missing-branch");        let (mappings, specs) = mapping_from_spec("refs/heads/unborn:refs/heads/valid-locally", &remote_repo);        assert_eq!(mappings.len(), 1);         let out = fetch::refs::update(            &local_repo,            prefixed("action"),            &mappings,            &specs,            &[],            fetch::Tags::None,            fetch::DryRun::Yes,            fetch::WritePackedRefs::Never,        )?;        assert_eq!(            out.updates,            vec![fetch::refs::Update {                mode: fetch::refs::update::Mode::RejectedToReplaceWithUnborn,                type_change: None,                edit_index: None            }]        );        assert_eq!(out.edits.len(), 0);        Ok(())    }     #[test]    fn unborn_remote_refs_dont_overwrite_valid_local_refs() -> Result {        let remote_repo = named_repo("unborn");        let local_repo = named_repo("one-commit-with-symref");        let (mappings, specs) =            mapping_from_spec("refs/heads/existing-unborn-symbolic:refs/heads/branch", &remote_repo);        assert_eq!(mappings.len(), 1);         let out = fetch::refs::update(            &local_repo,            prefixed("action"),            &mappings,            &specs,            &[],            fetch::Tags::None,            fetch::DryRun::Yes,            fetch::WritePackedRefs::Never,        )?;        assert_eq!(            out.updates,            vec![fetch::refs::Update {                mode: fetch::refs::update::Mode::RejectedToReplaceWithUnborn,                type_change: None,                edit_index: None            }],            "we don't overwrite locally present refs with unborn ones for safety"        );        assert_eq!(out.edits.len(), 0);        Ok(())    }     #[test]    fn local_symbolic_refs_can_be_overwritten() {        let repo = repo("two-origins");        let main_id = peeled_id(&repo, "refs/heads/main");        for (source, destination, expected_update, expected_edit) in [            (                // attempt to overwrite HEAD isn't possible as the matching engine will normalize the path. That way, `HEAD`                // can never be set. This is by design (of git) and we follow it.                "refs/heads/symbolic",                "HEAD",                fetch::refs::Update {                    mode: fetch::refs::update::Mode::New,

Lines 1–500 of 1012.