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.

Does this work with normal git? I vaguely remember some weird behaviour when pushing to repos of which you also care what is checked out. It may be the case that you're updating the branch but the HEAD is still set to the old commit.

2 Likes

I just tested the apply the same procedure with normal Git CLI and yes it worked as expected.

Here is what I entered:

$ mkdir test
$ cd test
~/test$ git init ./base
~/test$ git init --bare ./remote
~/test$ cd base/
~/test/base$ touch text.txt
~/test/base$ git add .
~/test/base$ git commit -m "initial"
~/test/base$ git remote add origin file:///home/peterrabbit/test/remote/
~/test/base$ git push origin HEAD
~/test/base$ cd ../
~/test$ git clone file:///home/peterrabbit/test/remote/ ./clone
~/test$ cd clone/
~/test/clone$ ls -l
total 0
-rw-r--r-- 1 ... 08:22 text.txt

This is exactly what I'm trying to do with git2.

While investigating I realized the problem might also be from the "add" part because when I stop the test just after calling add() and check into the test folder the git status with the normal GIT CLI I can see that the test.txt file has not been added .... So maybe the problem is there finally.

Here is my code that is meant to reproduce the git add . operation :


pub trait Git {
    // ...
    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()].iter(), git2::IndexAddOption::DEFAULT, None)?;
        index.write()?;
        Ok(index)
    }
    // ...
}

impl Git for TestGit {
   // ...
    fn git_add<'a, E: Into<&'a str>>(&self, expr: E) -> Result<git2::Index, git2::Error> {
        Self::_add_all(&self.dir, expr)
    }
    // ...
}

// ...

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

    testgit.git_init().unwrap();
    testgit.write_file("test.txt").unwrap();
    let index = testgit.git_add(".").unwrap();

    assert_eq!(index.len(), 1); // Maybe not the right way to ensure that changes were added ?

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

did you call index.write_tree() somewhere in your code? index.write() only write the index object, you also need to create the tree object that represents the current state of the index.

this git documentation describes what git add and git commit do under the hood:

https://git-scm.com/book/en/v2/Git-Internals-Git-Objects

1 Like

Thanks! Yes I forgot to call write_tree there.. it solved the add part,unfortunately it didn't solve the push problem....

since you didn't show your implementation, I can't be sure which API you might be missing, so I recreated these commands using libgit2, you can compare my API calls to your implementation to figure out the missing part.

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", "user@example.com").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();

	// add remote as "origin" and push the branch
	let mut origin = base_repo.remote("origin", &remote_url).unwrap();
	origin
		.push(&[branch.into_reference().name().unwrap()], 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()
	);
}
1 Like

there's an minor error in my previous code snippets, that is, after the commit, I only updated the branch pointer but forgot to update the HEAD pointer. you can run git checkout main to update the HEAD pointer, or you can modify the code a little bit, like so:

	// 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.into_reference().name().unwrap()], None)
-		.unwrap();
+	origin.push(&[branch_ref_name], None).unwrap();

note that, this is only needed for the initial commit which doesn't have parent commits. for later commits, simply use Some("HEAD") for the first argument of commit(), and provide the correct parent commit, like this:

	let commit_oid = repo
		.commit(Some("HEAD"), &author, &committer, "commit message", &tree, &[&parent])
		.unwrap();
1 Like

Thank you very much for that super clean example !

It solved the problem :slight_smile: .
I wasn't doing the update of the branch pointer. Seems like that was the missing part.

Just a question: is there a specific reason not to use the "master" branch in your example and use "main" instead ?

If you do a fresh git install it will default to "main" for, uh, main branch name. As to why it changed, I believe master-slave terminology is considered to be a bit insensitive these days.

1 Like