use git2::{BranchType, Repository, Signature};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tempfile::TempDir;
use crate::core::graph;
use crate::core::repo::{BranchInfo, CommitInfo, RemoteStatus, RepoInfo, UpstreamInfo};
use crate::core::shortid::IdAllocator;
use crate::core::status_json;
static IN_DIR_LOCK: Mutex<()> = Mutex::new(());
pub fn mentions_path(output: &str, path: &Path) -> bool {
let wanted = path.display().to_string().replace('\\', "/");
output.replace('\\', "/").contains(&wanted)
}
pub struct TestRepo {
pub repo: Repository,
_dir: TempDir,
}
impl TestRepo {
pub fn new() -> Self {
let dir = tempfile::tempdir().unwrap();
let repo = Repository::init(dir.path()).unwrap();
Self::configure_identity(&repo);
{
let sig = Self::sig();
let tree_id = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
.unwrap();
}
TestRepo { repo, _dir: dir }
}
pub fn new_empty() -> Self {
let dir = tempfile::tempdir().unwrap();
let repo = Repository::init(dir.path()).unwrap();
Self::configure_identity(&repo);
TestRepo { repo, _dir: dir }
}
pub fn new_with_remote() -> Self {
let dir = tempfile::tempdir().unwrap();
let remote_path = dir.path().join("remote.git");
let remote_repo = Repository::init_bare(&remote_path).unwrap();
remote_repo.set_head("refs/heads/main").unwrap();
{
let sig = Self::sig();
let tree_id = {
let mut index = remote_repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = remote_repo.find_tree(tree_id).unwrap();
remote_repo
.commit(Some("refs/heads/main"), &sig, &sig, "Initial", &tree, &[])
.unwrap();
}
let work_path = dir.path().join("work");
let repo = Repository::clone(remote_path.to_str().unwrap(), &work_path).unwrap();
Self::configure_identity(&repo);
{
let head_commit = repo.head().unwrap().peel_to_commit().unwrap();
repo.branch("integration", &head_commit, false).unwrap();
repo.set_head("refs/heads/integration").unwrap();
let mut integration = repo.find_branch("integration", BranchType::Local).unwrap();
integration.set_upstream(Some("origin/main")).unwrap();
}
TestRepo { repo, _dir: dir }
}
pub fn new_on_main_with_remote() -> Self {
let dir = tempfile::tempdir().unwrap();
let remote_path = dir.path().join("remote.git");
let remote_repo = Repository::init_bare(&remote_path).unwrap();
remote_repo.set_head("refs/heads/main").unwrap();
{
let sig = Self::sig();
let tree_id = {
let mut index = remote_repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = remote_repo.find_tree(tree_id).unwrap();
remote_repo
.commit(Some("refs/heads/main"), &sig, &sig, "Initial", &tree, &[])
.unwrap();
}
let work_path = dir.path().join("work");
let repo = Repository::clone(remote_path.to_str().unwrap(), &work_path).unwrap();
Self::configure_identity(&repo);
TestRepo { repo, _dir: dir }
}
fn configure_identity(repo: &Repository) {
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test").unwrap();
config.set_str("user.email", "test@test.com").unwrap();
config.set_str("core.editor", "false").unwrap();
config.set_str("core.autocrlf", "false").unwrap();
let hooks = repo.path().join("hooks").display().to_string();
config
.set_str("core.hooksPath", &hooks.replace('\\', "/"))
.unwrap();
}
fn sig() -> Signature<'static> {
Signature::now("Test", "test@test.com").unwrap()
}
pub fn commit(&self, message: &str, filename: &str) -> git2::Oid {
self.commit_with_sig(message, filename, &Self::sig())
}
pub fn commit_at(&self, message: &str, filename: &str, seconds: i64) -> git2::Oid {
let time = git2::Time::new(seconds, 0);
let sig = Signature::new("Test", "test@test.com", &time).unwrap();
self.commit_with_sig(message, filename, &sig)
}
fn commit_with_sig(&self, message: &str, filename: &str, sig: &Signature) -> git2::Oid {
let path = self.repo.workdir().unwrap().join(filename);
fs::write(&path, message).unwrap();
let mut index = self.repo.index().unwrap();
index.add_path(Path::new(filename)).unwrap();
index.write().unwrap();
let tree_id = index.write_tree().unwrap();
let tree = self.repo.find_tree(tree_id).unwrap();
match self.repo.head() {
Ok(head) => {
let parent = self.repo.find_commit(head.target().unwrap()).unwrap();
self.repo
.commit(Some("HEAD"), sig, sig, message, &tree, &[&parent])
.unwrap()
}
Err(_) => self
.repo
.commit(Some("HEAD"), sig, sig, message, &tree, &[])
.unwrap(),
}
}
pub fn commit_empty(&self, message: &str) -> git2::Oid {
let sig = Self::sig();
let tree_id = {
let mut index = self.repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = self.repo.find_tree(tree_id).unwrap();
if let Ok(head) = self.repo.head() {
let parent = self.repo.find_commit(head.target().unwrap()).unwrap();
self.repo
.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent])
.unwrap()
} else {
self.repo
.commit(Some("HEAD"), &sig, &sig, message, &tree, &[])
.unwrap()
}
}
pub fn commit_merge(
&self,
message: &str,
parent1_oid: git2::Oid,
parent2_oid: git2::Oid,
) -> git2::Oid {
let sig = Self::sig();
let p1 = self.repo.find_commit(parent1_oid).unwrap();
let p2 = self.repo.find_commit(parent2_oid).unwrap();
let tree = self.repo.find_tree(p1.tree_id()).unwrap();
self.repo
.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&p1, &p2])
.unwrap()
}
pub fn get_commit(&self, steps_back: usize) -> git2::Commit<'_> {
let mut commit = self.repo.head().unwrap().peel_to_commit().unwrap();
for _ in 0..steps_back {
commit = commit.parent(0).unwrap();
}
commit
}
pub fn head_commit(&self) -> git2::Commit<'_> {
self.get_commit(0)
}
pub fn get_message(&self, steps_back: usize) -> String {
self.get_commit(steps_back)
.message()
.unwrap()
.trim()
.to_string()
}
pub fn get_subject(&self, steps_back: usize) -> String {
crate::core::repo::commit_subject(&self.get_commit(steps_back))
}
pub fn get_oid(&self, steps_back: usize) -> git2::Oid {
self.get_commit(steps_back).id()
}
pub fn create_branch(&self, name: &str) -> git2::Branch<'_> {
let head_commit = self.head_commit();
self.repo.branch(name, &head_commit, false).unwrap()
}
pub fn workdir(&self) -> PathBuf {
self.repo.workdir().unwrap().to_path_buf()
}
pub fn in_dir<F, R>(&self, f: F) -> R
where
F: FnOnce() -> R,
{
let _lock = IN_DIR_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let restore = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
std::env::set_current_dir(self.workdir()).unwrap();
struct RestoreDir(PathBuf);
impl Drop for RestoreDir {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.0);
}
}
let _guard = RestoreDir(restore);
f()
}
pub fn in_dir_path<F, R>(&self, path: &Path, f: F) -> R
where
F: FnOnce() -> R,
{
let _lock = IN_DIR_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let restore = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
std::env::set_current_dir(path).unwrap();
struct RestoreDir(PathBuf);
impl Drop for RestoreDir {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.0);
}
}
let _guard = RestoreDir(restore);
f()
}
pub fn switch_branch(&self, name: &str) {
self.repo.set_head(&format!("refs/heads/{}", name)).unwrap();
self.repo
.checkout_head(Some(git2::build::CheckoutBuilder::new().force()))
.unwrap();
}
pub fn reset_hard(&self, oid: git2::Oid) {
let commit = self.find_commit(oid);
self.repo
.reset(commit.as_object(), git2::ResetType::Hard, None)
.unwrap();
}
pub fn force_checkout(&self) {
self.repo
.checkout_head(Some(git2::build::CheckoutBuilder::new().force()))
.unwrap();
}
pub fn create_branch_tracking(&self, name: &str, upstream: &str) {
let mut branch = self.create_branch(name);
branch.set_upstream(Some(upstream)).unwrap();
}
pub fn write_file(&self, filename: &str, content: &str) {
let path = self.workdir().join(filename);
fs::write(path, content).unwrap();
}
pub fn read_file(&self, filename: &str) -> String {
let path = self.workdir().join(filename);
fs::read_to_string(path).unwrap()
}
pub fn is_on_branch(&self) -> bool {
self.repo.head().unwrap().is_branch()
}
pub fn current_branch_name(&self) -> String {
self.repo.head().unwrap().shorthand().unwrap().to_string()
}
pub fn branch_exists(&self, name: &str) -> bool {
self.repo.find_branch(name, BranchType::Local).is_ok()
}
pub fn delete_branch(&self, name: &str) {
let workdir = self.workdir();
crate::git::branch_delete(workdir.as_path(), name).unwrap();
}
pub fn head_oid(&self) -> git2::Oid {
self.repo.head().unwrap().target().unwrap()
}
pub fn find_commit(&self, oid: git2::Oid) -> git2::Commit<'_> {
self.repo.find_commit(oid).unwrap()
}
pub fn create_branch_at_commit(&self, name: &str, oid: git2::Oid) -> git2::Branch<'_> {
let commit = self.find_commit(oid);
self.repo.branch(name, &commit, false).unwrap()
}
pub fn find_remote_branch_target(&self, name: &str) -> git2::Oid {
self.repo
.find_branch(name, BranchType::Remote)
.unwrap()
.get()
.target()
.unwrap()
}
pub fn get_branch_target(&self, name: &str) -> git2::Oid {
self.repo
.find_branch(name, BranchType::Local)
.unwrap()
.get()
.target()
.unwrap()
}
pub fn set_detached_head(&self, oid: git2::Oid) {
self.repo.set_head_detached(oid).unwrap();
}
pub fn set_fake_editor(&self, new_message: &str) {
let editor_script = format!("sh -c 'echo \"{}\" > \"$1\"' --", new_message);
self.set_config("core.editor", &editor_script);
}
pub fn remote_path(&self) -> Option<PathBuf> {
let remote_path = self._dir.path().join("remote.git");
if remote_path.exists() {
Some(remote_path)
} else {
None
}
}
pub fn add_remote_commits(&self, messages: &[&str]) -> git2::Oid {
let remote_path = self.remote_path().expect("No remote repository found");
let remote_repo = Repository::open_bare(&remote_path).unwrap();
let sig = Self::sig();
let mut last_oid = remote_repo
.find_branch("main", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
for message in messages {
let parent = remote_repo.find_commit(last_oid).unwrap();
let tree = parent.tree().unwrap();
last_oid = remote_repo
.commit(
Some("refs/heads/main"),
&sig,
&sig,
message,
&tree,
&[&parent],
)
.unwrap();
}
last_oid
}
pub fn cherry_pick_to_remote(&self, local_oid: git2::Oid, message: &str) -> git2::Oid {
let remote_path = self.remote_path().expect("No remote repository found");
let remote_repo = Repository::open_bare(&remote_path).unwrap();
let sig = Signature::now("Upstream", "upstream@test.com").unwrap();
let remote_tip = remote_repo
.find_branch("main", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
let parent = remote_repo.find_commit(remote_tip).unwrap();
let local_commit = self.repo.find_commit(local_oid).unwrap();
let local_tree = local_commit.tree().unwrap();
let local_parent_tree = local_commit.parent(0).unwrap().tree().unwrap();
let diff = self
.repo
.diff_tree_to_tree(Some(&local_parent_tree), Some(&local_tree), None)
.unwrap();
let remote_parent_tree = parent.tree().unwrap();
let mut builder = remote_repo.treebuilder(Some(&remote_parent_tree)).unwrap();
diff.foreach(
&mut |delta, _| {
match delta.status() {
git2::Delta::Added | git2::Delta::Modified => {
let new_file = delta.new_file();
let path = new_file.path().unwrap().to_str().unwrap();
let blob = self.repo.find_blob(new_file.id()).unwrap();
let new_blob = remote_repo.blob(blob.content()).unwrap();
builder.insert(path, new_blob, 0o100644).unwrap();
}
git2::Delta::Deleted => {
let old_file = delta.old_file();
let path = old_file.path().unwrap().to_str().unwrap();
builder.remove(path).unwrap();
}
_ => {}
}
true
},
None,
None,
None,
)
.unwrap();
let tree_oid = builder.write().unwrap();
let tree = remote_repo.find_tree(tree_oid).unwrap();
remote_repo
.commit(
Some("refs/heads/main"),
&sig,
&sig,
message,
&tree,
&[&parent],
)
.unwrap()
}
pub fn fetch_remote(&self) {
self.repo
.find_remote("origin")
.unwrap()
.fetch(&["main"], None, None)
.unwrap();
}
pub fn push_branch_to_remote_main(&self, branch: &str) {
crate::git::run_git(
self.workdir().as_path(),
&["push", "origin", &format!("{}:main", branch)],
)
.unwrap();
self.fetch_remote();
}
pub fn create_branch_at(&self, name: &str, commit_hash: &str) {
crate::git::branch_create(self.workdir().as_path(), name, commit_hash).unwrap();
}
pub fn merge_no_ff(&self, branch: &str) {
let outcome =
crate::git::merge_no_ff(self.workdir().as_path(), self.repo.path(), branch).unwrap();
assert!(
matches!(outcome, crate::git::MergeOutcome::Completed),
"merge_no_ff: expected Completed, got Stopped"
);
}
pub fn stage_files(&self, files: &[&str]) {
crate::git::stage_files(self.workdir().as_path(), files).unwrap();
}
pub fn commit_staged(&self, message: &str) {
crate::git::commit(self.workdir().as_path(), message).unwrap();
}
pub fn diff_head_name_only(&self) -> String {
crate::git::diff_head_name_only(self.workdir().as_path()).unwrap()
}
pub fn diff_commit(&self, oid: &str) -> String {
crate::git::diff_commit(self.workdir().as_path(), oid).unwrap()
}
pub fn set_config(&self, key: &str, value: &str) {
crate::git::run_git(self.workdir().as_path(), &["config", key, value]).unwrap();
}
#[cfg(unix)]
pub fn install_hook(&self, name: &str, body: &str) {
use std::os::unix::fs::PermissionsExt;
let hooks = self.repo.path().join("hooks");
std::fs::create_dir_all(&hooks).unwrap();
let hook = hooks.join(name);
std::fs::write(&hook, format!("#!/bin/sh\n{body}")).unwrap();
std::fs::set_permissions(&hook, std::fs::Permissions::from_mode(0o755)).unwrap();
self.set_config("core.hooksPath", hooks.to_str().unwrap());
}
pub fn status_porcelain(&self) -> String {
crate::git::run_git_stdout(self.workdir().as_path(), &["status", "--porcelain"]).unwrap()
}
pub fn rebase_onto(&self, newbase: &str, upstream: &str) {
crate::git::rebase_onto(self.workdir().as_path(), newbase, upstream).unwrap();
}
pub fn commit_messages(&self) -> Vec<String> {
let info = crate::core::repo::gather_repo_info(&self.repo, false, 1).unwrap();
info.commits.iter().map(|c| c.message.clone()).collect()
}
pub fn branch_names(&self) -> Vec<String> {
let info = crate::core::repo::gather_repo_info(&self.repo, false, 1).unwrap();
info.branches.iter().map(|b| b.name.clone()).collect()
}
pub fn branch_commit_summary(&self, name: &str) -> String {
let oid = self.get_branch_target(name);
let commit = self.find_commit(oid);
commit.summary().ok().flatten().unwrap_or("").to_string()
}
pub fn commit_file_paths(&self, oid: git2::Oid) -> Vec<String> {
crate::core::repo::commit_file_paths(&self.repo, oid).unwrap()
}
pub fn commit_has_file(&self, oid: git2::Oid, path: &str) -> bool {
self.find_commit(oid)
.tree()
.unwrap()
.get_path(Path::new(path))
.is_ok()
}
pub fn commit_multi(&self, files: &[(&str, &str)], message: &str) -> git2::Oid {
for (filename, content) in files {
self.write_file(filename, content);
}
let mut index = self.repo.index().unwrap();
for (filename, _) in files {
index.add_path(Path::new(filename)).unwrap();
}
index.write().unwrap();
let tree_id = index.write_tree().unwrap();
let tree = self.repo.find_tree(tree_id).unwrap();
let sig = Self::sig();
if let Ok(head) = self.repo.head() {
let parent = self.repo.find_commit(head.target().unwrap()).unwrap();
self.repo
.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent])
.unwrap()
} else {
self.repo
.commit(Some("HEAD"), &sig, &sig, message, &tree, &[])
.unwrap()
}
}
pub fn add_submodule(&self, path: &str) -> (git2::Oid, git2::Oid) {
let sub = Repository::init(self.workdir().join(path)).unwrap();
Self::configure_identity(&sub);
let first = Self::commit_in(&sub, "sub one");
let second = Self::commit_in(&sub, "sub two");
Self::detach(&sub, first);
self.write_file(
".gitmodules",
&format!("[submodule \"{path}\"]\n\tpath = {path}\n\turl = ./{path}\n"),
);
self.stage_files(&[".gitmodules", path]);
(first, second)
}
pub fn checkout_submodule(&self, path: &str, oid: git2::Oid) {
Self::detach(&Repository::open(self.workdir().join(path)).unwrap(), oid);
}
pub fn submodule_oid(&self, oid: git2::Oid, path: &str) -> git2::Oid {
self.find_commit(oid)
.tree()
.unwrap()
.get_path(Path::new(path))
.unwrap()
.id()
}
fn commit_in(repo: &Repository, message: &str) -> git2::Oid {
let name = format!("{}.txt", message.replace(' ', "-"));
fs::write(repo.workdir().unwrap().join(&name), message).unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new(&name)).unwrap();
index.write().unwrap();
let tree = repo.find_tree(index.write_tree().unwrap()).unwrap();
let sig = Self::sig();
let parents: Vec<git2::Commit> = repo
.head()
.ok()
.and_then(|head| head.target())
.map(|oid| repo.find_commit(oid).unwrap())
.into_iter()
.collect();
let parents: Vec<&git2::Commit> = parents.iter().collect();
repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &parents)
.unwrap()
}
fn detach(repo: &Repository, oid: git2::Oid) {
repo.set_head_detached(oid).unwrap();
repo.checkout_head(Some(git2::build::CheckoutBuilder::new().force()))
.unwrap();
}
pub fn assert_working_tree_clean(&self) {
let diff = self.diff_head_name_only();
assert!(
diff.trim().is_empty(),
"working tree should be clean, but has: {}",
diff
);
}
}
pub struct TestRepoBuilder {
repo: TestRepo,
}
impl TestRepoBuilder {
pub fn new() -> Self {
TestRepoBuilder {
repo: TestRepo::new_empty(),
}
}
pub fn with_initial_commit() -> Self {
TestRepoBuilder {
repo: TestRepo::new(),
}
}
pub fn commit(self, message: &str, filename: &str) -> Self {
self.repo.commit(message, filename);
self
}
pub fn branch(self, name: &str) -> Self {
self.repo.create_branch(name);
self
}
pub fn build(self) -> TestRepo {
self.repo
}
}
impl Default for TestRepoBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn repo_with_dropped_replay() -> (TestRepo, git2::Oid) {
let (t, target) = branch_forked_below_upstream();
publish_upstream_with_the_branch_content(&t);
(t, target)
}
pub fn repo_with_a_redundant_commit_below() -> (TestRepo, git2::Oid, git2::Oid) {
let (t, redundant) = branch_forked_below_upstream();
let keeper = t.commit_multi(&[("three.txt", "three\n")], "still needed");
publish_upstream_with_the_branch_content(&t);
(t, redundant, keeper)
}
pub fn repo_with_a_redundant_commit_between() -> (TestRepo, git2::Oid, git2::Oid) {
let t = TestRepo::new_with_remote();
let base = t.find_remote_branch_target("origin/main").to_string();
t.create_branch_at("alpha", &base);
t.switch_branch("alpha");
let older = t.commit_multi(
&[("moved.txt", "moved\n"), ("kept.txt", "kept\n")],
"older change",
);
t.commit_multi(
&[("one.txt", "final one\n"), ("two.txt", "final two\n")],
"branch change",
);
let newer = t.commit_multi(&[("newer.txt", "newer\n")], "newer change");
t.create_branch_at("upstream-work", &base);
publish_upstream_with_the_branch_content(&t);
(t, older, newer)
}
pub fn repo_with_a_redundant_commit_above() -> (TestRepo, git2::Oid) {
let t = TestRepo::new_with_remote();
let base = t.find_remote_branch_target("origin/main").to_string();
t.create_branch_at("alpha", &base);
t.switch_branch("alpha");
let keeper = t.commit_multi(&[("three.txt", "three\n")], "still needed");
t.commit_multi(
&[("one.txt", "final one\n"), ("two.txt", "final two\n")],
"branch change",
);
t.create_branch_at("upstream-work", &base);
publish_upstream_with_the_branch_content(&t);
(t, keeper)
}
fn publish_upstream_with_the_branch_content(t: &TestRepo) {
t.switch_branch("upstream-work");
t.commit_multi(&[("one.txt", "first try\n")], "upstream step one");
t.commit_multi(
&[("one.txt", "final one\n"), ("two.txt", "final two\n")],
"upstream step two",
);
weave_over_upstream(t);
}
pub fn repo_with_cherry_picked_replay() -> (TestRepo, git2::Oid) {
let (t, target) = branch_forked_below_upstream();
t.switch_branch("upstream-work");
t.commit_multi(&[("unrelated.txt", "unrelated\n")], "upstream work");
crate::git::run_git(t.workdir().as_path(), &["cherry-pick", &target.to_string()]).unwrap();
weave_over_upstream(&t);
(t, target)
}
fn branch_forked_below_upstream() -> (TestRepo, git2::Oid) {
let t = TestRepo::new_with_remote();
let base = t.find_remote_branch_target("origin/main").to_string();
t.create_branch_at("alpha", &base);
t.switch_branch("alpha");
let target = t.commit_multi(
&[("one.txt", "final one\n"), ("two.txt", "final two\n")],
"branch change",
);
t.create_branch_at("upstream-work", &base);
(t, target)
}
fn weave_over_upstream(t: &TestRepo) {
t.push_branch_to_remote_main("upstream-work");
t.switch_branch("integration");
t.reset_hard(t.find_remote_branch_target("origin/main"));
t.merge_no_ff("alpha");
}
pub fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\x1b' {
for inner in chars.by_ref() {
if inner == 'm' {
break;
}
}
} else {
out.push(c);
}
}
out
}
pub fn oid(byte: u8) -> git2::Oid {
let mut bytes = [0u8; 20];
bytes[0] = byte;
git2::Oid::from_bytes(&bytes).unwrap()
}
pub fn commit(byte: u8, message: &str, parent: Option<u8>) -> CommitInfo {
CommitInfo {
oid: oid(byte),
short_id: format!("{:07x}", byte),
message: message.to_string(),
change_id: None,
parent_oid: parent.map(oid),
files: vec![],
}
}
pub fn branch(name: &str, tip: u8, remote: Option<RemoteStatus>) -> BranchInfo {
BranchInfo {
name: name.to_string(),
tip_oid: oid(tip),
remote,
}
}
pub fn base_info() -> RepoInfo {
RepoInfo {
branch_name: "integration".to_string(),
upstream: UpstreamInfo {
label: "origin/main".to_string(),
tip_oid: oid(0xAA),
base_short_id: "aaa0000".to_string(),
base_message: "Initial commit".to_string(),
base_date: "2025-07-06".to_string(),
commits_ahead: 0,
merge_base_oid: oid(0xAA),
},
commits: vec![],
branches: vec![],
working_changes: vec![],
context_commits: vec![],
}
}
pub fn status_graph(info: RepoInfo) -> status_json::StatusGraph {
status_graph_in(info, "")
}
pub fn status_graph_in(info: RepoInfo, cwd_prefix: &str) -> status_json::StatusGraph {
let ids = IdAllocator::new(info.collect_entities());
let branch = info.branch_name.clone();
let stacks = stack_edges(&info);
status_json::build(
&graph::build_sections(info),
&branch,
&stacks,
&ids,
cwd_prefix,
)
}
pub fn stack_edges(info: &RepoInfo) -> HashMap<String, status_json::StackEdge> {
graph::stack_parents(info)
.into_iter()
.map(|(b, below)| {
let edge = status_json::StackEdge {
below: Some(below),
below_hidden: false,
};
(b, edge)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_repo_creation() {
let repo = TestRepo::new();
assert!(repo.is_on_branch());
assert_eq!(repo.get_message(0), "Initial commit");
}
#[test]
fn test_commit_and_get() {
let repo = TestRepo::new();
repo.commit("Second commit", "file2.txt");
repo.commit("Third commit", "file3.txt");
assert_eq!(repo.get_message(0), "Third commit");
assert_eq!(repo.get_message(1), "Second commit");
assert_eq!(repo.get_message(2), "Initial commit");
}
#[test]
fn test_builder_pattern() {
let repo = TestRepoBuilder::with_initial_commit()
.commit("Second", "file2.txt")
.commit("Third", "file3.txt")
.branch("feature")
.build();
assert_eq!(repo.get_message(0), "Third");
assert_eq!(repo.get_message(1), "Second");
assert!(
repo.repo
.find_branch("feature", git2::BranchType::Local)
.is_ok()
);
}
#[test]
fn test_file_operations() {
let repo = TestRepo::new();
repo.write_file("test.txt", "hello");
assert_eq!(repo.read_file("test.txt"), "hello");
}
}