use git2::{
Error, ReferenceType, Repository, RepositoryInitOptions, StashFlags, Status, StatusOptions,
};
use libgit2_sys as raw;
use std::ffi::{CString, OsString};
use std::fs;
use std::path::Path;
use std::ptr;
use tempfile::TempDir;
#[test]
#[cfg_attr(any(windows, target_os = "macos"), ignore)]
fn non_utf8_branch() {
let td = TempDir::new().unwrap();
let path = td.path();
{
let mut opts = RepositoryInitOptions::new();
opts.initial_head("main");
let repo = Repository::init_opts(path, &opts).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "name").unwrap();
config.set_str("user.email", "email").unwrap();
let mut index = repo.index().unwrap();
let id = index.write_tree().unwrap();
let tree = repo.find_tree(id).unwrap();
let sig = repo.signature().unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[])
.unwrap();
}
{
macro_rules! check_result {
($result:ident) => {
if $result != 0 {
let e = Error::last_error($result);
assert_eq!("", format!("{:?}", e));
}
};
}
#[cfg(unix)]
fn ostr_to_cstr(s: OsString) -> CString {
use std::ffi::OsStr;
use std::os::unix::prelude::*;
let s: &OsStr = s.as_ref();
CString::new(s.as_bytes()).unwrap()
}
#[cfg(windows)]
fn ostr_to_cstr(s: OsString) -> CString {
panic!("Test is skipped on Windows");
}
let path = ostr_to_cstr(path.into());
let mut repo = ptr::null_mut();
unsafe {
let result = raw::git_repository_open(&mut repo, path.as_ptr());
assert_eq!(0, result);
}
let mut head_reference = ptr::null_mut();
unsafe {
let result = raw::git_repository_head(&mut head_reference, repo);
check_result!(result);
}
let mut peeled = ptr::null_mut();
unsafe {
let result =
raw::git_reference_peel(&mut peeled, head_reference, raw::GIT_OBJECT_COMMIT);
check_result!(result);
assert_eq!(raw::GIT_OBJECT_COMMIT, raw::git_object_type(&*peeled));
}
let as_commit = peeled as *mut raw::git_commit;
let branch_name = CString::new(vec![b'f', 0xff, 0xC0, b'o', b'o']).unwrap();
let mut branch_reference = ptr::null_mut();
unsafe {
let result = raw::git_branch_create(
&mut branch_reference,
repo,
branch_name.as_ptr(),
as_commit,
0,
);
check_result!(result);
}
unsafe {
raw::git_reference_free(branch_reference);
raw::git_commit_free(as_commit);
raw::git_object_free(peeled);
raw::git_reference_free(head_reference);
raw::git_repository_free(repo);
}
}
let repo = Repository::open(path).expect("created above");
let mut refs = repo.references().expect("references");
let mut names = refs.names();
assert_eq!(
Some(Err(Error::from_str(
"invalid utf-8 sequence of 1 bytes from index 12"
))),
names.next()
);
assert_eq!(Some(Ok("refs/heads/main")), names.next());
assert_eq!(None, names.next());
}
#[test]
fn stash_length() {
let td = TempDir::new().unwrap();
let path = td.path();
let mut opts = RepositoryInitOptions::new();
opts.initial_head("main");
let mut repo = Repository::init_opts(path, &opts).unwrap();
let initial_reflog = repo.reflog("refs/stash").expect("Should work");
assert_eq!(0, initial_reflog.len());
let mut config = repo.config().unwrap();
config.set_str("user.name", "name").unwrap();
config.set_str("user.email", "email").unwrap();
let sig = repo.signature().unwrap();
let mut index = repo.index().unwrap();
let id = index.write_tree().unwrap();
{
let tree = repo.find_tree(id).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[])
.unwrap();
}
fs::write(path.join("README.md"), "README content").unwrap();
repo.stash_save(&sig, "Stashed", Some(StashFlags::INCLUDE_UNTRACKED))
.unwrap();
assert_eq!(0, initial_reflog.len());
let after_stash1 = repo.reflog("refs/stash").expect("Should work");
assert_eq!(1, after_stash1.len());
fs::write(path.join("README.md2"), "README content").unwrap();
repo.stash_save(&sig, "Stashed2", Some(StashFlags::INCLUDE_UNTRACKED))
.unwrap();
assert_eq!(0, initial_reflog.len());
assert_eq!(1, after_stash1.len());
let after_stash2 = repo.reflog("refs/stash").expect("Should work");
assert_eq!(2, after_stash2.len());
repo.stash_drop(1).expect("Should succeed");
assert_eq!(0, initial_reflog.len());
assert_eq!(1, after_stash1.len());
assert_eq!(2, after_stash2.len());
let after_drop1 = repo.reflog("refs/stash").expect("Should work");
assert_eq!(1, after_drop1.len());
repo.stash_drop(0).expect("Should succeed");
assert_eq!(0, initial_reflog.len());
assert_eq!(1, after_stash1.len());
assert_eq!(2, after_stash2.len());
assert_eq!(1, after_drop1.len());
let after_drop2 = repo.reflog("refs/stash").expect("Should work");
assert_eq!(0, after_drop2.len());
}
#[test]
fn branch_name_on_init() {
{
let td = TempDir::new().unwrap();
let path = td.path();
let mut opts = RepositoryInitOptions::new();
opts.initial_head("main");
let repo = Repository::init_opts(path, &opts).unwrap();
let head_ref = repo.find_reference("HEAD").unwrap();
assert_eq!(Some(ReferenceType::Symbolic), head_ref.kind());
assert_eq!(Ok("HEAD"), head_ref.name());
let target = head_ref.symbolic_target();
assert_eq!(Ok(Some("refs/heads/main")), target);
}
{
let td = TempDir::new().unwrap();
let path = td.path();
let mut opts = RepositoryInitOptions::new();
opts.initial_head("somerandombranchnamehere");
let repo = Repository::init_opts(path, &opts).unwrap();
let head_ref = repo.find_reference("HEAD").unwrap();
assert_eq!(Some(ReferenceType::Symbolic), head_ref.kind());
assert_eq!(Ok("HEAD"), head_ref.name());
let target = head_ref.symbolic_target();
assert_eq!(Ok(Some("refs/heads/somerandombranchnamehere")), target);
}
}
#[test]
fn repo_status() {
let td = TempDir::new().unwrap();
let path = td.path();
let mut opts = RepositoryInitOptions::new();
opts.initial_head("main");
let repo = Repository::init_opts(path, &opts).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "name").unwrap();
config.set_str("user.email", "email").unwrap();
fs::write(path.join("BothModified"), "BothModified").unwrap();
fs::write(path.join("IndexModified"), "IndexModified").unwrap();
fs::write(path.join("IndexDeleted"), "IndexDeleted").unwrap();
fs::write(path.join("IndexRenamed"), "IndexRenamed").unwrap();
fs::write(path.join("IndexTypechange"), "IndexTypechange").unwrap();
fs::write(path.join("WorktreeDeleted"), "WorktreeDeleted").unwrap();
fs::write(path.join("WorktreeModified"), "WorktreeModified").unwrap();
fs::write(path.join("WorktreeTypechange"), "WorktreeTypechange").unwrap();
fs::write(path.join("WorktreeRenamed"), "WorktreeRenamed").unwrap();
fs::write(path.join("Unchanged"), "Unchanged").unwrap();
fs::write(path.join(".gitignore"), "ignored-*").unwrap();
let mut index = repo.index().unwrap();
index.add_path(&Path::new("BothModified")).unwrap();
index.add_path(&Path::new("IndexModified")).unwrap();
index.add_path(&Path::new("IndexDeleted")).unwrap();
index.add_path(&Path::new("IndexRenamed")).unwrap();
index.add_path(&Path::new("IndexTypechange")).unwrap();
index.add_path(&Path::new("Unchanged")).unwrap();
index.add_path(&Path::new("WorktreeDeleted")).unwrap();
index.add_path(&Path::new("WorktreeModified")).unwrap();
index.add_path(&Path::new("WorktreeRenamed")).unwrap();
index.add_path(&Path::new("WorktreeTypechange")).unwrap();
index.add_path(&Path::new(".gitignore")).unwrap();
let id = index.write_tree().unwrap();
let tree = repo.find_tree(id).unwrap();
let sig = repo.signature().unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "Initial files", &tree, &[])
.unwrap();
fs::write(path.join("BothModified"), "Modified in index").unwrap();
fs::write(path.join("IndexModified"), "IndexModified-content2").unwrap();
fs::write(path.join("IndexNew"), "IndexNew").unwrap();
fs::remove_file(path.join("IndexDeleted")).unwrap();
fs::remove_file(path.join("IndexTypechange")).unwrap();
fn create_symlink(to: &Path, from: &Path) {
#[cfg(unix)]
std::os::unix::fs::symlink(to, from).unwrap();
#[cfg(windows)]
std::os::windows::fs::symlink_file(to, from).unwrap();
}
create_symlink(&path.join("Unchanged"), &path.join("IndexTypechange"));
fs::rename(path.join("IndexRenamed"), path.join("IndexRenamed-new")).unwrap();
index.add_path(&Path::new("BothModified")).unwrap();
index.remove_path(&Path::new("IndexDeleted")).unwrap();
index.add_path(&Path::new("IndexModified")).unwrap();
index.add_path(&Path::new("IndexNew")).unwrap();
index.remove_path(&Path::new("IndexRenamed")).unwrap();
index.add_path(&Path::new("IndexRenamed-new")).unwrap();
index.add_path(&Path::new("IndexTypechange")).unwrap();
fs::write(path.join("ignored-random"), "ignored-random").unwrap();
fs::write(path.join("BothModified"), "Modified in worktree").unwrap();
fs::remove_file(path.join("WorktreeDeleted")).unwrap();
fs::write(path.join("WorktreeModified"), "New content").unwrap();
fs::write(path.join("WorktreeNew"), "WorktreeNew").unwrap();
fs::remove_file(path.join("WorktreeTypechange")).unwrap();
fs::rename(
path.join("WorktreeRenamed"),
path.join("WorktreeRenamed-new"),
)
.unwrap();
create_symlink(&path.join("Unchanged"), &path.join("WorktreeTypechange"));
let mut opts = StatusOptions::new();
opts.renames_head_to_index(true);
opts.renames_index_to_workdir(true);
opts.include_untracked(true);
opts.include_unmodified(true);
opts.include_ignored(true);
let status = repo.statuses(Some(&mut opts)).unwrap();
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
struct SimpleEntry {
path: String,
status: String,
}
let mut entries: Vec<SimpleEntry> = vec![];
for entry in status.iter() {
entries.push({
SimpleEntry {
path: entry.path().unwrap().to_string(),
status: format!("{:?}", entry.status()),
}
});
}
entries.sort();
macro_rules! expected {
($($path:literal -> $status:expr,)+) => {
vec![
$(SimpleEntry { path: $path.to_string(), status: concat!("Status(", stringify!($status), ")").to_string() }),+
]
}
}
assert_eq!(
expected!(
".gitignore" -> 0x0,
"BothModified" -> INDEX_MODIFIED | WT_MODIFIED,
"IndexDeleted" -> INDEX_DELETED,
"IndexModified" -> INDEX_MODIFIED,
"IndexNew" -> INDEX_NEW,
"IndexRenamed" -> INDEX_RENAMED,
"IndexTypechange" -> INDEX_TYPECHANGE,
"Unchanged" -> 0x0,
"WorktreeDeleted" -> WT_DELETED,
"WorktreeModified" -> WT_MODIFIED,
"WorktreeNew" -> WT_NEW,
"WorktreeRenamed" -> WT_RENAMED,
"WorktreeTypechange" -> WT_TYPECHANGE,
"ignored-random" -> IGNORED,
),
entries
);
}
#[test]
fn repo_status_clean() {
let td = TempDir::new().unwrap();
let path = td.path();
let mut opts = RepositoryInitOptions::new();
opts.initial_head("main");
let repo = Repository::init_opts(path, &opts).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "name").unwrap();
config.set_str("user.email", "email").unwrap();
fs::write(path.join("MyFile"), "content").unwrap();
let mut index = repo.index().unwrap();
index.add_path(&Path::new("MyFile")).unwrap();
let id = index.write_tree().unwrap();
let tree = repo.find_tree(id).unwrap();
let sig = repo.signature().unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "Initial files", &tree, &[])
.unwrap();
let mut opts = StatusOptions::new();
opts.include_untracked(true);
{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(0, status.len());
}
fs::write(path.join("OtherFile"), "content").unwrap();
{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(1, status.len());
let entry = status.get(0).unwrap();
assert_eq!(Ok("OtherFile"), entry.path());
assert_eq!(Status::WT_NEW, entry.status());
}
index.add_path(&Path::new("OtherFile")).unwrap();
{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(1, status.len());
let entry = status.get(0).unwrap();
assert_eq!(Ok("OtherFile"), entry.path());
assert_eq!(Status::INDEX_NEW, entry.status());
}
fs::remove_file(path.join("OtherFile")).unwrap();
{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(1, status.len());
let entry = status.get(0).unwrap();
assert_eq!(Ok("OtherFile"), entry.path());
assert_eq!(Status::INDEX_NEW | Status::WT_DELETED, entry.status());
}
index.remove_path(&Path::new("OtherFile")).unwrap();
{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(0, status.len());
}
}