Search code examples
rustlibgit2git2-rs

How to use git2::Remote::push correctly?


I'm building an app which internally uses git2.rs to manage a project.

I'm trying to implement tests for basic use cases such as git init, git add, commit and push to a remote and I have problems with the pushing part.

I implemented my test case using a local bare remote repository. I first create a source repository, init git inside of it, then I create a dumb text file, add it to the index and commit it.

Everything seems to work until there.

Then I create a local bare repo, I set it as the "origin" remote for the source repo and I call push on the remote repo instance. I have no errors but the content of the source repo doesn't seems to be pushed.

The documentation is not very learner friendly so I have troubles understanding what I'm doing.

I would expect maybe to see my text file somewhere is the remote repo directory but there is only the git structure.

And when I try to make an assertion by cloning the remote into a new directoryy after pushing I check if the text file is there, but it's not, it just creates an empty repository.


Here is the relevant part of my code, it's just a trait which I implement in the tests submodule.

The source trait

use git2::Repository;
use std::path::PathBuf;

pub trait Git {
    // ... other methods...

    fn _set_remote<'a, T: Into<PathBuf>>(
        repo_dir: T,
        name: &str,
        url: &str,
    ) -> Result<(), git2::Error> {
        let repo = Self::_repo(repo_dir)?;
        repo.remote(name, url)?;
        Ok(())
    }

    fn git_init(&self) -> Result<Repository, git2::Error>;
    fn git_add<'a, E: Into<&'a str>>(&self, expr: E) -> Result<git2::Index, git2::Error>;
    fn git_commit<'a, M: Into<&'a str>>(&self, message: M) -> Result<git2::Oid, git2::Error>;
    fn git_set_remote(&self, name: &str, url: &str) -> Result<(), git2::Error>;
}

The tests implementation

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;

    struct TestGit {
        pub dir: PathBuf,
        pub state: String,
    }

   // Impl TestGit ...

    impl Git for TestGit {
        fn git_init(&self) -> Result<Repository, git2::Error> {
            // ... 
        }

        fn git_add<'a, E: Into<&'a str>>(&self, expr: E) -> Result<git2::Index, git2::Error> {
            // ...
        }

        fn git_commit<'a, M: Into<&'a str>>(&self, message: M) -> Result<git2::Oid, git2::Error> {
            // ...
        }

        fn git_set_remote(&self, name: &str, url: &str) -> Result<(), git2::Error> {
            Self::_set_remote(&self.dir, name, url)
        }
    }

   // Some first tests for init, add, commit, write file, etc.
   // ...

    #[test]
    fn test_push() {
        let testgit = TestGit {
            dir: std::env::current_dir().unwrap().join("test/base"),
            state: String::from("Hello"),
        };

        let base_repo = testgit.git_init().unwrap();

        let testgitremote = create_testgit_instance("test/remote");
        <TestGit as Git>::_init::<&PathBuf>(&testgitremote.dir, true).unwrap();

        testgit
            .git_set_remote(
                "origin",
                format!("file://{}", testgitremote.dir.to_str().unwrap()).as_str(),
            )
            .unwrap();

        testgit.write_file("test.txt").unwrap(); // This creates a test.txt file with "Hello" in it at the root of the repo. 

        testgit.git_add(".").unwrap();
        testgit.git_commit("test commit").unwrap();
        // This works find until there becauses I tested it elsewhere, the index contains one more element after the commit.

        let mut remote = base_repo.find_remote("origin").unwrap();

        remote.push::<&str>(&[], None).unwrap(); // This is what I'm having troubles to understand, I'm guessing I'm just pushing nothing but I don't find anything clear in the docs and there is no "push" example it the git2.rs sources.

        let mut clonebuilder = git2::build::RepoBuilder::new();

        let clonerepo_dir = testgit.dir.parent().unwrap().join("clone");

        clonebuilder
            .clone(remote.url().unwrap(), &clonerepo_dir)
            .unwrap();

        assert!(clonerepo_dir.join("test.txt").exists()); // This fails...

        std::fs::remove_dir_all(&testgit.dir.parent().unwrap()).unwrap();
    }
}

I also tried to add refspecs like this but it doesn't changed anything

let mut remote = base_repo.find_remote("origin").unwrap();

remote.push::<&str>(&["refs/heads/master:refs/heads/master")], None).unwrap();

Or like this, same result.

let mut remote = base_repo.find_remote("origin").unwrap();

base_repo
    .remote_add_push("origin", "refs/heads/master:refs/heads/master")
    .unwrap();

remote.push::<&str>(&[], None).unwrap();

Thank you very much for any help.


Solution

  • I got an solution in this thread https://users.rust-lang.org/t/how-to-use-git2-push-correctly/97202/6 , I rely it here in case it could be useful.

    It turned out the problem was from my git commit implementation. I forgot to update the branch pointer with the new commit. That's why nothing was pushed.

    This is the snippet that gave me the solution

    use std::{fs, path};
    
    use git2::build::RepoBuilder;
    use git2::{IndexAddOption, Repository, Signature};
    
    
    fn main() {
        let root_dir = path::Path::new("Z:/Temp");
        let base_path = root_dir.join("base");
        let remote_path = root_dir.join("remote");
        let clone_path = root_dir.join("clone");
        let author = Signature::now("user", "[email protected]").unwrap();
    
        // create base repo and remote bare repo
        let base_repo = Repository::init(&base_path).unwrap();
        let remote_repo = Repository::init_bare(&remote_path).unwrap();
        let remote_url = format!("file:///{}", remote_repo.path().display());
    
        // create a text file and add it to index
        fs::write(base_path.join("hello.txt"), "hello world!\n").unwrap();
        let mut base_index = base_repo.index().unwrap();
        base_index
            .add_all(["."], IndexAddOption::DEFAULT, None)
            .unwrap();
        base_index.write().unwrap();
    
        // make the commit, since it's the initial commit, there's no parent
        let tree = base_repo
            .find_tree(base_index.write_tree().unwrap())
            .unwrap();
        let commit_oid = base_repo
            .commit(None, &author, &author, "initial", &tree, &[])
            .unwrap();
    
        // update branch pointer
        let branch = base_repo
            .branch("main", &base_repo.find_commit(commit_oid).unwrap(), true)
            .unwrap();
        let branch_ref = branch.into_reference();
        let branch_ref_name = branch_ref.name().unwrap();
        base_repo.set_head(branch_ref_name).unwrap();
    
        // add remote as "origin" and push the branch
        let mut origin = base_repo.remote("origin", &remote_url).unwrap();
        origin.push(&[branch_ref_name], None).unwrap();
    
        // clone from remote
        let clone_repo = RepoBuilder::new()
            .branch("main")
            .clone(&remote_url, &clone_path)
            .unwrap();
    
        // examine the commit message:
        println!(
            "short commit message: {}",
            clone_repo
                .head()
                .unwrap()
                .peel_to_commit()
                .unwrap()
                .summary()
                .unwrap()
        );
    }
    

    If useful, here is my fixed implementation of add and commit, and the push test.

        fn _add_all<'a, T: Into<PathBuf>, E: Into<&'a str>>(
            repo_dir: T,
            expr: E,
        ) -> Result<git2::Index, git2::Error> {
            let repo = Self::_repo(repo_dir)?;
            let mut index = repo.index()?;
            index.add_all([expr.into()], git2::IndexAddOption::DEFAULT, None)?;
            index.write()?;
            index.write_tree()?;
            Ok(index)
        }
    
        fn _update_branch<'a, T: Into<PathBuf>, Str: Into<&'a str>>(
            repo_dir: T,
            name: Str,
            commit_oid: &git2::Oid,
        ) -> Result<(), git2::Error> {
            let repo = Self::_repo(repo_dir)?;
            let branch = repo.branch(name.into(), &repo.find_commit(commit_oid.clone())?, true)?;
            let branch_ref = branch.into_reference();
            let branch_ref_name = branch_ref.name().unwrap();
            repo.set_head(branch_ref_name)?;
            Ok(())
        }
    
        fn _commit<'a, T: Into<PathBuf>, Str: Into<&'a str>>(
            repo_dir: T,
            message: Str,
            update_branch: Str,
        ) -> Result<git2::Oid, git2::Error> {
            let repo_dir: PathBuf = repo_dir.into();
            let repo = Self::_repo(&repo_dir)?;
            let mut index = repo.index()?;
            let sign = Self::_signature(&repo)?;
            let tree = repo.find_tree(index.write_tree()?)?;
    
            let mut parents = vec![];
            let mut update_ref = Some("HEAD");
    
            if let Ok(head) = repo.head() {
                parents.push(head.peel_to_commit()?);
            } else {
                update_ref = None; // no HEAD = first commit
            }
    
            let oid = repo.commit(
                update_ref,
                &sign,
                &sign,
                message.into(),
                &tree,
                &parents.iter().collect::<Vec<&git2::Commit>>()[..],
            )?;
    
            Self::_update_branch(repo_dir, update_branch.into(), &oid)?;
    
            Ok(oid)
        }
    
        #[test]
        fn test_push() {
            let testgit = TestGit {
                dir: std::env::current_dir().unwrap().join("test/base"),
                state: String::from("Hello"),
            };
            let base_repo = testgit.git_init().unwrap();
    
            let testgitremote = TestGit {
                dir: std::env::current_dir().unwrap().join("test/remote"),
                state: String::from("Hello"),
            };
            <TestGit as Git>::_init::<&PathBuf>(&testgitremote.dir, true).unwrap();
    
            testgit
                .git_set_remote(
                    "origin",
                    format!("file://{}", testgitremote.dir.to_str().unwrap()).as_str(),
                )
                .unwrap();
    
            testgit.write_file("test.txt").unwrap();
    
            testgit.git_add(".").unwrap();
    
            testgit.git_commit("test commit", "master").unwrap();
    
            let master = base_repo
                .find_branch("master", git2::BranchType::Local)
                .unwrap();
    
            let mut remote = base_repo.find_remote("origin").unwrap();
    
            remote
                .push::<&str>(&[master.into_reference().name().unwrap()], None)
                .unwrap();
    
            let mut clonebuilder = git2::build::RepoBuilder::new();
    
            let clonerepo_dir = testgit.dir.parent().unwrap().join("clone");
    
            clonebuilder
                .clone(remote.url().unwrap(), &clonerepo_dir)
                .unwrap();
    
            assert!(clonerepo_dir.join("test.txt").exists());
    
            std::fs::remove_dir_all(&testgit.dir.parent().unwrap()).unwrap();
        }