//! Tests for supporting older versions of the Cargo.lock file format.

use crate::prelude::*;
use cargo_test_support::compare::assert_e2e;
use cargo_test_support::git;
use cargo_test_support::registry::Package;
use cargo_test_support::str;
use cargo_test_support::{basic_lib_manifest, basic_manifest, project};

#[cargo_test]
fn oldest_lockfile_still_works() {
    let cargo_commands = vec!["build", "update"];
    for cargo_command in cargo_commands {
        oldest_lockfile_still_works_with_command(cargo_command);
    }
}

fn oldest_lockfile_still_works_with_command(cargo_command: &str) {
    Package::new("bar", "0.1.0").publish();

    let expected_lockfile = str![[r##"
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "[..]"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar",
]

"##]];

    let old_lockfile = r#"
[root]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
"#;

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", old_lockfile)
        .build();

    p.cargo(cargo_command).run();

    let lock = p.read_lockfile();
    assert_e2e().eq(&lock, expected_lockfile);
}

#[cargo_test]
fn frozen_flag_preserves_old_lockfile() {
    let cksum = Package::new("bar", "0.1.0").publish();

    let old_lockfile = format!(
        r#"[root]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[metadata]
"checksum bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "{}"
"#,
        cksum,
    );

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", &old_lockfile)
        .build();

    p.cargo("check --locked").run();

    let lock = p.read_lockfile();
    assert_e2e().eq(&lock, &old_lockfile);
}

#[cargo_test]
fn totally_wild_checksums_works() {
    Package::new("bar", "0.1.0").publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .file(
            "Cargo.lock",
            r#"
[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[metadata]
"checksum baz 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "checksum"
"checksum bar 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "checksum"
"#,
        );

    let p = p.build();

    p.cargo("check").run();

    let lock = p.read_lockfile();
    assert_e2e().eq(
        &lock,
        str![[r##"
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "[..]"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar",
]

"##]],
    );
}

#[cargo_test]
fn wrong_checksum_is_an_error() {
    Package::new("bar", "0.1.0").publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .file(
            "Cargo.lock",
            r#"
[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[metadata]
"checksum bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "checksum"
"#,
        );

    let p = p.build();

    p.cargo("check")
        .with_status(101)
        .with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] checksum for `bar v0.1.0` changed between lock files

this could be indicative of a few possible errors:

    * the lock file is corrupt
    * a replacement source in use (e.g., a mirror) returned a different checksum
    * the source itself may be corrupt in one way or another

unable to verify that `bar v0.1.0` is the same as when the lockfile was generated


"#]])
        .run();
}

// If the checksum is unlisted in the lock file (e.g., <none>) yet we can
// calculate it (e.g., it's a registry dep), then we should in theory just fill
// it in.
#[cargo_test]
fn unlisted_checksum_is_bad_if_we_calculate() {
    Package::new("bar", "0.1.0").publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .file(
            "Cargo.lock",
            r#"
[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[metadata]
"checksum bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "<none>"
"#,
        );
    let p = p.build();

    p.cargo("fetch").with_status(101).with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] checksum for `bar v0.1.0` was not previously calculated, but a checksum could now be calculated

this could be indicative of a few possible situations:

    * the source `registry `crates-io`` did not previously support checksums,
      but was replaced with one that does
    * newer Cargo implementations know how to checksum this source, but this
      older implementation does not
    * the lock file is corrupt


"#]]).run();
}

// If the checksum is listed in the lock file yet we cannot calculate it (e.g.,
// Git dependencies as of today), then make sure we choke.
#[cargo_test]
fn listed_checksum_bad_if_we_cannot_compute() {
    let git = git::new("bar", |p| {
        p.file("Cargo.toml", &basic_manifest("bar", "0.1.0"))
            .file("src/lib.rs", "")
    });

    let p = project()
        .file(
            "Cargo.toml",
            &format!(
                r#"
                    [package]
                    name = "foo"
                    version = "0.0.1"
                    edition = "2015"
                    authors = []

                    [dependencies]
                    bar = {{ git = '{}' }}
                "#,
                git.url()
            ),
        )
        .file("src/lib.rs", "")
        .file(
            "Cargo.lock",
            &format!(
                r#"
[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar 0.1.0 (git+{0})"
]

[[package]]
name = "bar"
version = "0.1.0"
source = "git+{0}"

[metadata]
"checksum bar 0.1.0 (git+{0})" = "checksum"
"#,
                git.url()
            ),
        );

    let p = p.build();

    p.cargo("fetch").with_status(101).with_stderr_data(str![[r#"
[UPDATING] git repository `[ROOTURL]/bar`
[ERROR] checksum for `bar v0.1.0 ([ROOTURL]/bar)` could not be calculated, but a checksum is listed in the existing lock file

this could be indicative of a few possible situations:

    * the source `[ROOTURL]/bar` supports checksums,
      but was replaced with one that doesn't
    * the lock file is corrupt

unable to verify that `bar v0.1.0 ([ROOTURL]/bar)` is the same as when the lockfile was generated


"#]]).run();
}

#[cargo_test]
fn current_lockfile_format() {
    Package::new("bar", "0.1.0").publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "");
    let p = p.build();

    p.cargo("check").run();

    let actual = p.read_lockfile();

    let expected = str![[r##"
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "[..]"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar",
]

"##]];
    assert_e2e().eq(&actual, expected);
}

#[cargo_test]
fn lockfile_without_root() {
    Package::new("bar", "0.1.0").publish();

    let lockfile = r#"
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar",
]
"#;

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", lockfile);

    let p = p.build();

    p.cargo("check").run();

    let lock = p.read_lockfile();
    assert_e2e().eq(
        &lock,
        str![[r##"
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "[..]"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar",
]

"##]],
    );
}

#[cargo_test]
fn locked_correct_error() {
    Package::new("bar", "0.1.0").publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "");
    let p = p.build();

    p.cargo("check --locked")
        .with_status(101)
        .with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] cannot create the lock file [ROOT]/foo/Cargo.lock because --locked was passed to prevent this
[HELP] to generate the lock file without accessing the network, remove the --locked flag and use --offline instead.

"#]])
        .run();
}

#[cargo_test]
fn v2_format_preserved() {
    let cksum = Package::new("bar", "0.1.0").publish();

    let lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "{}"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar",
]
"#,
        cksum
    );

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", &lockfile)
        .build();

    p.cargo("fetch").run();

    let lock = p.read_lockfile();
    assert_e2e().eq(&lock, &lockfile);
}

#[cargo_test]
fn v2_path_and_crates_io() {
    let cksum010 = Package::new("a", "0.1.0").publish();
    let cksum020 = Package::new("a", "0.2.0").publish();

    let lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "a"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "{}"

[[package]]
name = "a"
version = "0.2.0"

[[package]]
name = "a"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "{}"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "a 0.1.0",
 "a 0.2.0",
 "a 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
"#,
        cksum010, cksum020,
    );

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                a = { path = 'a' }
                b = { version = "0.1", package = 'a' }
                c = { version = "0.2", package = 'a' }
            "#,
        )
        .file("src/lib.rs", "")
        .file(
            "a/Cargo.toml",
            r#"
                [package]
                name = "a"
                version = "0.2.0"
                edition = "2015"
            "#,
        )
        .file("a/src/lib.rs", "")
        .file("Cargo.lock", &lockfile)
        .build();

    p.cargo("fetch").run();
    p.cargo("fetch").run();

    let lock = p.read_lockfile();
    assert_e2e().eq(&lock, &lockfile);
}

#[cargo_test]
fn v3_and_git() {
    let (git_project, repo) = git::new_repo("dep1", |project| {
        project
            .file("Cargo.toml", &basic_lib_manifest("dep1"))
            .file("src/lib.rs", "")
    });
    let head_id = repo.head().unwrap().target().unwrap();

    let lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "dep1"
version = "0.5.0"
source = "git+[ROOTURL]/dep1?branch=master#{}"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "dep1",
]
"#,
        head_id,
    );

    let p = project()
        .file(
            "Cargo.toml",
            &format!(
                r#"
                    [package]
                    name = "foo"
                    version = "0.0.1"
                    edition = "2015"
                    rust-version = "1.81" # ensure it stays in lockfile v3

                    [dependencies]
                    dep1 = {{ git = '{}', branch = 'master' }}
                "#,
                git_project.url(),
            ),
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", "version = 3")
        .build();

    p.cargo("fetch").run();

    let lock = p.read_lockfile();
    assert_e2e().eq(&lock, &lockfile);
}

#[cargo_test]
fn lock_from_the_future() {
    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []
            "#,
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", "version = 10000000")
        .build();

    p.cargo("fetch").with_stderr_data(str![[r#"
[ERROR] failed to parse lock file at: [ROOT]/foo/Cargo.lock

Caused by:
  lock file version `10000000` was found, but this version of Cargo does not understand this lock file, perhaps Cargo needs to be updated?

"#]]).with_status(101).run();
}

#[cargo_test]
fn preserve_old_format_if_no_update_needed() {
    let cksum = Package::new("bar", "0.1.0").publish();
    let lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[metadata]
"checksum bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "{}"
"#,
        cksum
    );

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                authors = []

                [dependencies]
                bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", &lockfile)
        .build();

    p.cargo("check --locked").run();
}

#[cargo_test]
fn same_name_version_different_sources() {
    let cksum = Package::new("foo", "0.1.0").publish();
    let (git_project, repo) = git::new_repo("dep1", |project| {
        project
            .file(
                "Cargo.toml",
                r#"
                    [package]
                    name = "foo"
                    version = "0.1.0"
                    edition = "2015"
                "#,
            )
            .file("src/lib.rs", "")
    });
    let head_id = repo.head().unwrap().target().unwrap();

    // Lockfile was generated with Rust 1.51
    let lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "foo"
version = "0.1.0"
dependencies = [
 "foo 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "foo 0.1.0 (git+{url})",
]

[[package]]
name = "foo"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "{cksum}"

[[package]]
name = "foo"
version = "0.1.0"
source = "git+{url}#{sha}"
"#,
        sha = head_id,
        url = git_project.url(),
        cksum = cksum
    );

    let p = project()
        .file(
            "Cargo.toml",
            &format!(
                r#"
                    [package]
                    name = "foo"
                    version = "0.1.0"
                    edition = "2015"

                    [dependencies]
                    foo = "0.1.0"
                    foo2 = {{ git = '{}', package = 'foo' }}
                "#,
                git_project.url(),
            ),
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", &lockfile)
        .build();

    p.cargo("check").run();

    assert_eq!(p.read_file("Cargo.lock"), lockfile);
}

#[cargo_test]
fn bad_data_in_lockfile_error_meg() {
    Package::new("bar", "0.0.1").publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "test"
                version = "0.0.0"
                edition = "2015"

                [dependencies]
                bar = "*"
            "#,
        )
        .file("src/main.rs", "fn main() {}")
        .file(
            "Cargo.lock",
            r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1b9346248cf3391ead604c4407258d327c28e37209f6d56127598165165dda"

[[package]]
name = "test"
version = "0.0.0"
dependencies = [
 "bar",
]"#,
        )
        .build();
    p.cargo("check")
        .with_status(101)
        .with_stderr_data(str![[r#"
[UPDATING] `dummy-registry` index
[ERROR] failed to select a version for the requirement `bar = "*"` (locked to 0.1.0)
candidate versions found which didn't match: 0.0.1
location searched: `dummy-registry` index (which is replacing registry `crates-io`)
required by package `test v0.0.0 ([ROOT]/foo)`
perhaps a crate was updated and forgotten to be re-vendored?

"#]])
        .run();
}

#[cargo_test]
fn next_version_is_always_unstable() {
    let p = project()
        .file(
            "Cargo.toml",
            &format!(
                r#"
                    [package]
                    name = "foo"
                    version = "0.0.1"
                    edition = "2015"
                "#,
            ),
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", "version = 5")
        .build();

    p.cargo("fetch").with_status(101).with_stderr_data(str![[r#"
[ERROR] failed to parse lock file at: [ROOT]/foo/Cargo.lock

Caused by:
  lock file version `5` was found, but this version of Cargo does not understand this lock file, perhaps Cargo needs to be updated?

"#]]).run();

    // On nightly, let the user know about the `-Z` flag.
    p.cargo("fetch")
        .masquerade_as_nightly_cargo(&["-Znext-lockfile-bump"])
        .with_status(101)
        .with_stderr_data(str![[r#"
[ERROR] failed to parse lock file at: [ROOT]/foo/Cargo.lock

Caused by:
  lock file version `5` requires `-Znext-lockfile-bump`

"#]])
        .run();
}

fn create_branch(repo: &git2::Repository, branch: &str, head_id: git2::Oid) {
    repo.branch(branch, &repo.find_commit(head_id).unwrap(), true)
        .unwrap();
}

fn create_tag(repo: &git2::Repository, tag: &str, head_id: git2::Oid) {
    repo.tag(
        tag,
        &repo.find_object(head_id, None).unwrap(),
        &repo.signature().unwrap(),
        "make a new tag",
        false,
    )
    .unwrap();
}

fn v3_and_git_url_encoded(ref_kind: &str, f: impl FnOnce(&git2::Repository, &str, git2::Oid)) {
    let (git_project, repo) = git::new_repo("dep1", |project| {
        project
            .file("Cargo.toml", &basic_lib_manifest("dep1"))
            .file("src/lib.rs", "")
    });
    let url = git_project.url();
    let head_id = repo.head().unwrap().target().unwrap();
    // Ref name with special characters
    let git_ref = "a-_+#$)";
    let encoded_ref = "a-_%2B%23%24%29";
    f(&repo, git_ref, head_id);

    let lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "dep1"
version = "0.5.0"
source = "git+[ROOTURL]/dep1?{ref_kind}={git_ref}#{head_id}"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "dep1",
]
"#,
    );

    let p = project()
        .file(
            "Cargo.toml",
            &format!(
                r#"
                    [package]
                    name = "foo"
                    version = "0.0.1"
                    edition = "2015"
                    rust-version = "1.81" # ensure it stays in lockfile v3

                    [dependencies]
                    dep1 = {{ git = '{url}', {ref_kind} = '{git_ref}' }}
                "#,
            ),
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", "version = 3")
        .build();

    p.cargo("check")
        .with_stderr_data(format!(
            "\
[UPDATING] git repository `[ROOTURL]/dep1`
[LOCKING] 1 package to latest compatible version
[ADDING] dep1 v0.5.0 ([ROOTURL]/dep1?{ref_kind}={encoded_ref}#[..])
[CHECKING] dep1 v0.5.0 ([ROOTURL]/dep1?{ref_kind}={encoded_ref}#[..])
[CHECKING] foo v0.0.1 ([ROOT]/foo)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
"
        ))
        .run();

    let lock = p.read_lockfile();
    assert_e2e().eq(&lock, &lockfile);

    // v3 doesn't URL-encode URL parameters, but `url` crate does decode as it
    // was URL-encoded. Therefore Cargo thinks they are from different source
    // and clones the repository again.
    p.cargo("check")
        .with_stderr_data(format!(
            "\
[UPDATING] git repository `[ROOTURL]/dep1`
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
"
        ))
        .run();
}

#[cargo_test]
fn v3_and_git_url_encoded_branch() {
    v3_and_git_url_encoded("branch", create_branch);
}

#[cargo_test]
fn v3_and_git_url_encoded_tag() {
    v3_and_git_url_encoded("tag", create_tag);
}

#[cargo_test]
fn v3_and_git_url_encoded_rev() {
    v3_and_git_url_encoded("rev", create_tag);
}

fn v4_and_git_url_encoded(ref_kind: &str, f: impl FnOnce(&git2::Repository, &str, git2::Oid)) {
    let (git_project, repo) = git::new_repo("dep1", |project| {
        project
            .file("Cargo.toml", &basic_lib_manifest("dep1"))
            .file("src/lib.rs", "")
    });
    let url = git_project.url();
    let head_id = repo.head().unwrap().target().unwrap();
    // Ref name with special characters
    let git_ref = "a-_+#$)";
    let encoded_ref = "a-_%2B%23%24%29";
    f(&repo, git_ref, head_id);

    let lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "dep1"
version = "0.5.0"
source = "git+[ROOTURL]/dep1?{ref_kind}={encoded_ref}#{head_id}"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "dep1",
]
"#,
    );

    let p = project()
        .file(
            "Cargo.toml",
            &format!(
                r#"
                    [package]
                    name = "foo"
                    version = "0.0.1"
                    edition = "2015"

                    [dependencies]
                    dep1 = {{ git = '{url}', {ref_kind} = '{git_ref}' }}
                "#,
            ),
        )
        .file("src/lib.rs", "")
        .file("Cargo.lock", "version = 4")
        .build();

    p.cargo("check")
        .with_stderr_data(format!(
            "\
[UPDATING] git repository `[ROOTURL]/dep1`
[LOCKING] 1 package to latest compatible version
[ADDING] dep1 v0.5.0 ([ROOTURL]/dep1?{ref_kind}={encoded_ref}#[..])
[CHECKING] dep1 v0.5.0 ([ROOTURL]/dep1?{ref_kind}={encoded_ref}#[..])
[CHECKING] foo v0.0.1 ([ROOT]/foo)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
"
        ))
        .run();

    let lock = p.read_lockfile();
    assert_e2e().eq(&lock, &lockfile);

    // Unlike v3_and_git_url_encoded, v4 encodes URL parameters so no git
    // repository re-clone happen.
    p.cargo("check")
        .with_stderr_data(
            "\
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
",
        )
        .run();
}

#[cargo_test]
fn v4_and_git_url_encoded_branch() {
    v4_and_git_url_encoded("branch", create_branch);
}

#[cargo_test]
fn v4_and_git_url_encoded_tag() {
    v4_and_git_url_encoded("tag", create_tag);
}

#[cargo_test]
fn v4_and_git_url_encoded_rev() {
    v4_and_git_url_encoded("rev", create_tag)
}

#[cargo_test]
fn with_msrv() {
    let cksum = Package::new("bar", "0.1.0").publish();

    let v3_lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "{cksum}"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar",
]
"#
    );
    let v2_lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "{cksum}"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar",
]
"#
    );

    let v1_lockfile = format!(
        r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "bar"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "foo"
version = "0.0.1"
dependencies = [
 "bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[metadata]
"checksum bar 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "{cksum}"
"#
    );

    let p = project()
        .file(
            "Cargo.toml",
            r#"
            [package]
            name = "foo"
            version = "0.0.1"
            edition = "2015"

            [dependencies]
            bar = "0.1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .build();

    let cases = [
        // v1 is the default
        ("1.37", None, 1),
        ("1.37", Some(1), 1),
        ("1.37", Some(2), 2),
        ("1.37", Some(3), 3),
        ("1.37", Some(4), 4),
        // v2 introduced
        ("1.38", None, 1),
        // last version of v1 as the default
        ("1.40", None, 1),
        // v2 is the default
        ("1.41", None, 2),
        ("1.41", Some(1), 1),
        ("1.41", Some(2), 2),
        ("1.41", Some(3), 3),
        ("1.41", Some(4), 4),
        // v3 introduced
        ("1.47", None, 2),
        // last version of v2 as the default
        ("1.48", None, 2),
        // v3 is the default
        ("1.53", None, 3),
        ("1.53", Some(1), 1),
        ("1.53", Some(2), 2),
        ("1.53", Some(3), 3),
        ("1.53", Some(4), 4),
        // v4 introduced
        ("1.78", None, 3),
        // last version of v3 as the default
        ("1.82", None, 3),
        // v4 is the default
        ("1.83", None, 4),
        ("1.83", Some(1), 1),
        ("1.83", Some(2), 2),
        ("1.83", Some(3), 3),
        ("1.83", Some(4), 4),
    ];

    for (msrv, existing_lockfile, expected_version) in cases {
        // Clean previous lockfile.
        _ = std::fs::remove_file(p.root().join("Cargo.lock"));

        p.change_file(
            "Cargo.toml",
            &format!(
                r#"
                [package]
                name = "foo"
                version = "0.0.1"
                edition = "2015"
                rust-version = "{msrv}"

                [dependencies]
                bar = "0.1.0"
                "#,
            ),
        );

        if let Some(existing_lockfile) = existing_lockfile {
            let existing_lockfile = match existing_lockfile {
                1 => v1_lockfile.as_str().into(),
                2 => v2_lockfile.as_str().into(),
                3 => v3_lockfile.as_str().into(),
                v => std::borrow::Cow::from(format!("version = {v}")),
            };
            p.change_file("Cargo.lock", &existing_lockfile);
        }

        p.cargo("fetch").run();

        let lock = p.read_lockfile();
        let toml = lock.parse::<toml::Table>().unwrap();
        // get `version = <n>` from Cargo.lock
        let version_field = toml.get("version").and_then(|v| v.as_integer());

        let actual_version = if let Some(ver) = version_field {
            ver
        } else if lock.find("\nchecksum = ").is_some() {
            2
        } else {
            1
        };

        assert_eq!(
            expected_version, actual_version,
            "msrv: {msrv}, existing lockfile: {existing_lockfile:?}"
        );
    }
}
