#
cargo_test_support/
git.rs

    1//! # Git Testing Support
    2//!
    3//! ## Creating a git dependency
    4//! [`new()`] is an easy way to create a new git repository containing a
    5//! project that you can then use as a dependency. It will automatically add all
    6//! the files you specify in the project and commit them to the repository.
    7//!
    8//! ### Example:
    9//!
    10//! ```no_run
    11//! # use cargo_test_support::project;
    12//! # use cargo_test_support::basic_manifest;
    13//! # use cargo_test_support::git;
    14//! let git_project = git::new("dep1", |project| {
    15//!     project
    16//!         .file("Cargo.toml", &basic_manifest("dep1", "1.0.0"))
    17//!         .file("src/lib.rs", r#"pub fn f() { println!("hi!"); } "#)
    18//! });
    19//!
    20//! // Use the `url()` method to get the file url to the new repository.
    21//! let p = project()
    22//!     .file("Cargo.toml", &format!(r#"
    23//!         [package]
    24//!         name = "a"
    25//!         version = "1.0.0"
    26//!
    27//!         [dependencies]
    28//!         dep1 = {{ git = '{}' }}
    29//!     "#, git_project.url()))
    30//!     .file("src/lib.rs", "extern crate dep1;")
    31//!     .build();
    32//! ```
    33//!
    34//! ## Manually creating repositories
    35//!
    36//! [`repo()`] can be used to create a [`RepoBuilder`] which provides a way of
    37//! adding files to a blank repository and committing them.
    38//!
    39//! If you want to then manipulate the repository (such as adding new files or
    40//! tags), you can use `git2::Repository::open()` to open the repository and then
    41//! use some of the helper functions in this file to interact with the repository.
    42
    43use crate::{Project, ProjectBuilder, SymlinkBuilder, paths::CargoPathExt, project};
    44use std::fs;
    45use std::path::{Path, PathBuf};
    46use std::sync::Once;
    47use url::Url;
    48
    49/// Manually construct a [`Repository`]
    50///
    51/// See also [`new`], [`repo`]
    52#[must_use]
    53pub struct RepoBuilder {
    54    repo: git2::Repository,
    55    files: Vec<PathBuf>,
    56}
    57
    58/// See [`new`]
    59pub struct Repository(git2::Repository);
    60
    61/// Create a [`RepoBuilder`] to build a new git repository.
    62///
    63/// Call [`RepoBuilder::build()`] to finalize and create the repository.
    64pub fn repo(p: &Path) -> RepoBuilder {
    65    RepoBuilder::init(p)
    66}
    67
    68impl RepoBuilder {
    69    pub fn init(p: &Path) -> RepoBuilder {
    70        t!(fs::create_dir_all(p.parent().unwrap()));
    71        let repo = init(p);
    72        RepoBuilder {
    73            repo,
    74            files: Vec::new(),
    75        }
    76    }
    77
    78    /// Add a file to the repository.
    79    pub fn file(self, path: &str, contents: &str) -> RepoBuilder {
    80        let mut me = self.nocommit_file(path, contents);
    81        me.files.push(PathBuf::from(path));
    82        me
    83    }
    84
    85    /// Create a symlink to a directory
    86    pub fn nocommit_symlink_dir<T: AsRef<Path>>(self, dst: T, src: T) -> Self {
    87        let workdir = self.repo.workdir().unwrap();
    88        SymlinkBuilder::new_dir(workdir.join(dst), workdir.join(src)).mk();
    89        self
    90    }
    91
    92    /// Add a file that will be left in the working directory, but not added
    93    /// to the repository.
    94    pub fn nocommit_file(self, path: &str, contents: &str) -> RepoBuilder {
    95        let dst = self.repo.workdir().unwrap().join(path);
    96        t!(fs::create_dir_all(dst.parent().unwrap()));
    97        t!(fs::write(&dst, contents));
    98        self
    99    }
    100
    101    /// Create the repository and commit the new files.
    102    pub fn build(self) -> Repository {
    103        {
    104            let mut index = t!(self.repo.index());
    105            for file in self.files.iter() {
    106                t!(index.add_path(file));
    107            }
    108            t!(index.write());
    109            let id = t!(index.write_tree());
    110            let tree = t!(self.repo.find_tree(id));
    111            let sig = t!(self.repo.signature());
    112            t!(self
    113                .repo
    114                .commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[]));
    115        }
    116        let RepoBuilder { repo, .. } = self;
    117        Repository(repo)
    118    }
    119}
    120
    121impl Repository {
    122    pub fn root(&self) -> &Path {
    123        self.0.workdir().unwrap()
    124    }
    125
    126    pub fn url(&self) -> Url {
    127        self.0.workdir().unwrap().to_url()
    128    }
    129
    130    pub fn revparse_head(&self) -> String {
    131        self.0
    132            .revparse_single("HEAD")
    133            .expect("revparse HEAD")
    134            .id()
    135            .to_string()
    136    }
    137}
    138
    139/// *(`git2`)* Initialize a new repository at the given path.
    140pub fn init(path: &Path) -> git2::Repository {
    141    default_search_path();
    142    let repo = t!(git2::Repository::init(path));
    143    default_repo_cfg(&repo);
    144    repo
    145}
    146
    147fn default_search_path() {
    148    use crate::paths::global_root;
    149    use git2::{ConfigLevel, opts::set_search_path};
    150
    151    static INIT: Once = Once::new();
    152    INIT.call_once(|| unsafe {
    153        let path = global_root().join("blank_git_search_path");
    154        t!(set_search_path(ConfigLevel::System, &path));
    155        t!(set_search_path(ConfigLevel::Global, &path));
    156        t!(set_search_path(ConfigLevel::XDG, &path));
    157        t!(set_search_path(ConfigLevel::ProgramData, &path));
    158    })
    159}
    160
    161fn default_repo_cfg(repo: &git2::Repository) {
    162    let mut cfg = t!(repo.config());
    163    t!(cfg.set_str("user.email", "foo@bar.com"));
    164    t!(cfg.set_str("user.name", "Foo Bar"));
    165}
    166
    167/// Create a new [`Project`] in a git [`Repository`]
    168pub fn new<F>(name: &str, callback: F) -> Project
    169where
    170    F: FnOnce(ProjectBuilder) -> ProjectBuilder,
    171{
    172    new_repo(name, callback).0
    173}
    174
    175/// Create a new [`Project`] with access to the [`Repository`]
    176pub fn new_repo<F>(name: &str, callback: F) -> (Project, git2::Repository)
    177where
    178    F: FnOnce(ProjectBuilder) -> ProjectBuilder,
    179{
    180    let mut git_project = project().at(name);
    181    git_project = callback(git_project);
    182    let git_project = git_project.build();
    183
    184    let repo = init(&git_project.root());
    185    add(&repo);
    186    commit(&repo);
    187    (git_project, repo)
    188}
    189
    190/// *(`git2`)* Add all files in the working directory to the git index
    191pub fn add(repo: &git2::Repository) {
    192    let mut index = t!(repo.index());
    193    t!(index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None));
    194    t!(index.write());
    195}
    196
    197/// *(`git2`)* Add a git submodule to the repository
    198pub fn add_submodule<'a>(
    199    repo: &'a git2::Repository,
    200    url: &str,
    201    path: &Path,
    202) -> git2::Submodule<'a> {
    203    let path = path.to_str().unwrap().replace(r"\", "/");
    204    let mut s = t!(repo.submodule(url, Path::new(&path), false));
    205    let subrepo = t!(s.open());
    206    default_repo_cfg(&subrepo);
    207    t!(subrepo.remote_add_fetch("origin", "refs/heads/*:refs/heads/*"));
    208    let mut origin = t!(subrepo.find_remote("origin"));
    209    t!(origin.fetch(&Vec::<String>::new(), None, None));
    210    t!(subrepo.checkout_head(None));
    211    t!(s.add_finalize());
    212    s
    213}
    214
    215/// *(`git2`)* Commit changes to the git repository
    216pub fn commit(repo: &git2::Repository) -> git2::Oid {
    217    let tree_id = t!(t!(repo.index()).write_tree());
    218    let sig = t!(repo.signature());
    219    let mut parents = Vec::new();
    220    if let Some(parent) = repo.head().ok().map(|h| h.target().unwrap()) {
    221        parents.push(t!(repo.find_commit(parent)))
    222    }
    223    let parents = parents.iter().collect::<Vec<_>>();
    224    t!(repo.commit(
    225        Some("HEAD"),
    226        &sig,
    227        &sig,
    228        "test",
    229        &t!(repo.find_tree(tree_id)),
    230        &parents
    231    ))
    232}
    233
    234/// *(`git2`)* Create a new tag in the git repository
    235pub fn tag(repo: &git2::Repository, name: &str) {
    236    let head = repo.head().unwrap().target().unwrap();
    237    t!(repo.tag(
    238        name,
    239        &t!(repo.find_object(head, None)),
    240        &t!(repo.signature()),
    241        "make a new tag",
    242        false
    243    ));
    244}
    245
    246/// Returns true if gitoxide is globally activated.
    247///
    248/// That way, tests that normally use `git2` can transparently use `gitoxide`.
    249pub fn cargo_uses_gitoxide() -> bool {
    250    std::env::var_os("__CARGO_USE_GITOXIDE_INSTEAD_OF_GIT2").map_or(false, |value| value == "1")
    251}