Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 229 additions & 1 deletion tests/end_to_end.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
//! Tests for some end-to-end logic about certain operations
use git2::{Error, ReferenceType, Repository, RepositoryInitOptions, StashFlags};
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;
Expand Down Expand Up @@ -238,3 +241,228 @@ fn branch_name_on_init() {
assert_eq!(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();

// Create some files
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();

// Modify some files that will differ between HEAD and index
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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On windows this requires developer mode privileges btw. If we have already done this, fine, if not, better make this skippable locally and enforced in CI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't appear to have done this already - not sure how to make it skippable locally and enforced in CI, even though I use a windows machine I do all of my development in docker containers that run linux.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
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();

// And between index and worktree
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() }),+
]
}
}
// Does not cover WT_UNREADABLE which is rare, or CONFLICTED which is for
// in-progress conflicts
// Doesn't show all combinations of index and worktree changes, just a few
assert_eq!(
expected!(
// Status::CURRENT
".gitignore" -> 0x0,
"BothModified" -> INDEX_MODIFIED | WT_MODIFIED,
"IndexDeleted" -> INDEX_DELETED,
"IndexModified" -> INDEX_MODIFIED,
"IndexNew" -> INDEX_NEW,
"IndexRenamed" -> INDEX_RENAMED,
"IndexTypechange" -> INDEX_TYPECHANGE,
// Status::CURRENT
"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();

// Create some files
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();

// A repo status is "clean" if
// - there are no untracked files
// - there are no modified files, in either the index or worktree
let mut opts = StatusOptions::new();
opts.include_untracked(true);

// Repo is clean:
{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(0, status.len());
}

fs::write(path.join("OtherFile"), "content").unwrap();

// Repo is dirty due to the untracked file
{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(1, status.len());
let entry = status.get(0).unwrap();
assert_eq!(Some("OtherFile"), entry.path());
assert_eq!(Status::WT_NEW, entry.status());
}

// Add it to the index
index.add_path(&Path::new("OtherFile")).unwrap();

// Still dirty
{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(1, status.len());
let entry = status.get(0).unwrap();
assert_eq!(Some("OtherFile"), entry.path());
assert_eq!(Status::INDEX_NEW, entry.status());
}

// Remove the file,
fs::remove_file(path.join("OtherFile")).unwrap();

// Still dirty because it is in the index
{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(1, status.len());
let entry = status.get(0).unwrap();
assert_eq!(Some("OtherFile"), entry.path());
assert_eq!(Status::INDEX_NEW | Status::WT_DELETED, entry.status());
}

// After removing from the index, it should be clean again
index.remove_path(&Path::new("OtherFile")).unwrap();

{
let status = repo.statuses(Some(&mut opts)).unwrap();
assert_eq!(0, status.len());
}
}
Loading