use std::path::Path;
use anyhow::{Context, Result};
use clap::Parser;
#[derive(Parser)]
pub struct ViewCommand {
#[arg(value_name = "COMMIT_RANGE")]
pub commit_range: Option<String>,
}
impl ViewCommand {
pub fn execute(self) -> Result<()> {
let commit_range = self.commit_range.as_deref().unwrap_or("HEAD");
let yaml_output = run_view(commit_range, None::<&str>)?;
println!("{yaml_output}");
Ok(())
}
}
pub fn run_view<P: AsRef<Path>>(commit_range: &str, repo_path: Option<P>) -> Result<String> {
use crate::data::{
AiInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo, WorkingDirectoryInfo,
};
use crate::git::{GitRepository, RemoteInfo};
use crate::utils::ai_scratch;
let repo = if let Some(path) = repo_path {
GitRepository::open_at(path).context("Failed to open git repository at the given path")?
} else {
crate::utils::check_git_repository()?;
GitRepository::open()
.context("Failed to open git repository. Make sure you're in a git repository.")?
};
let wd_status = repo.get_working_directory_status()?;
let working_directory = WorkingDirectoryInfo {
clean: wd_status.clean,
untracked_changes: wd_status
.untracked_changes
.into_iter()
.map(|fs| FileStatusInfo {
status: fs.status,
file: fs.file,
})
.collect(),
};
let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
let commits = repo.get_commits_in_range(commit_range)?;
let versions = Some(VersionInfo {
omni_dev: env!("CARGO_PKG_VERSION").to_string(),
});
let ai_scratch_path =
ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
let ai_info = AiInfo {
scratch: ai_scratch_path.to_string_lossy().to_string(),
};
let mut repo_view = RepositoryView {
versions,
explanation: FieldExplanation::default(),
working_directory,
remotes,
ai: ai_info,
branch_info: None,
pr_template: None,
pr_template_location: None,
branch_prs: None,
commits,
};
repo_view.to_yaml_output()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use git2::{Repository, Signature};
use tempfile::TempDir;
fn init_repo_with_commits() -> (TempDir, Vec<git2::Oid>) {
let tmp_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tmp");
std::fs::create_dir_all(&tmp_root).unwrap();
let temp_dir = tempfile::tempdir_in(&tmp_root).unwrap();
let repo_path = temp_dir.path();
let repo = Repository::init(repo_path).unwrap();
{
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
}
let signature = Signature::now("Test", "test@example.com").unwrap();
let mut commits = Vec::new();
for (i, msg) in ["feat: one", "fix: two"].iter().enumerate() {
std::fs::write(repo_path.join("f.txt"), format!("c{i}")).unwrap();
let mut idx = repo.index().unwrap();
idx.add_path(std::path::Path::new("f.txt")).unwrap();
idx.write().unwrap();
let tree_id = idx.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let parents: Vec<git2::Commit<'_>> = match commits.last() {
Some(id) => vec![repo.find_commit(*id).unwrap()],
None => vec![],
};
let parent_refs: Vec<&git2::Commit<'_>> = parents.iter().collect();
let oid = repo
.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
&parent_refs,
)
.unwrap();
commits.push(oid);
}
(temp_dir, commits)
}
#[test]
fn run_view_with_explicit_path_returns_yaml_with_commits() {
let (temp_dir, _commits) = init_repo_with_commits();
let yaml = run_view("HEAD~1..HEAD", Some(temp_dir.path())).unwrap();
assert!(yaml.contains("commits:"), "yaml lacks commits: {yaml}");
assert!(yaml.contains("fix: two"), "yaml missing latest: {yaml}");
}
#[test]
fn run_view_default_head_returns_latest_commit() {
let (temp_dir, _commits) = init_repo_with_commits();
let yaml = run_view("HEAD", Some(temp_dir.path())).unwrap();
assert!(yaml.contains("fix: two"));
}
#[test]
fn run_view_with_invalid_path_returns_error() {
let err = run_view("HEAD", Some("/no/such/path/exists")).unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.to_lowercase().contains("git") || msg.to_lowercase().contains("repo"),
"expected git/repo error, got: {msg}"
);
}
use crate::cli::git::CWD_MUTEX;
#[test]
fn execute_uses_cwd_repo_and_succeeds() {
let _guard = CWD_MUTEX.blocking_lock();
let (temp_dir, _commits) = init_repo_with_commits();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = ViewCommand {
commit_range: Some("HEAD".to_string()),
}
.execute();
std::env::set_current_dir(original_cwd).unwrap();
result.expect("execute should succeed in a valid repo");
}
#[test]
fn execute_default_range_uses_head() {
let _guard = CWD_MUTEX.blocking_lock();
let (temp_dir, _commits) = init_repo_with_commits();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = ViewCommand { commit_range: None }.execute();
std::env::set_current_dir(original_cwd).unwrap();
result.expect("execute with default range should succeed");
}
#[test]
fn run_view_includes_untracked_changes_in_output() {
let (temp_dir, _commits) = init_repo_with_commits();
std::fs::write(temp_dir.path().join("untracked.txt"), "stray").unwrap();
let yaml = run_view("HEAD", Some(temp_dir.path())).unwrap();
assert!(
yaml.contains("untracked.txt"),
"expected untracked.txt in output, got: {yaml}"
);
assert!(
yaml.contains("clean: false"),
"expected dirty status: {yaml}"
);
}
}