From 503674ddacbff533e73d7735d6db67b598d5dc48 Mon Sep 17 00:00:00 2001 From: Gregorios Leach Date: Mon, 4 May 2026 22:46:24 -0700 Subject: [PATCH] fix: sandbox-friendly git tests and linked-gitdir support - Integration tests use git init --separate-git-dir, scratch under target/, local gpgsign disabled, and cleanup of sibling .git dir. - Resolve real git directory via rev-parse for rebase detection and pgit-sync-state when using separate git directories. Add AGENTS.md with build, test, and clippy/fmt commands. Co-authored-by: Cursor --- AGENTS.md | 13 ++++++++++++ src/git/ops.rs | 38 ++++++++++++++++++++++++++++++---- tests/test_git_ops.rs | 48 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e8dfbac --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +This is a utility for managing stacks of Pull Requests in various git providers (GitHub, GitLab, etc), written in Rust. + +Build, test and run static analysis checks before committing anything. + +Build: + * cargo build + +Test: + * cargo test + +Static analysis: + * cargo clippy --all-targets --all-features -- -D warnings + * cargo fmt --all -- --check diff --git a/src/git/ops.rs b/src/git/ops.rs index fbe56aa..2086e8f 100644 --- a/src/git/ops.rs +++ b/src/git/ops.rs @@ -420,10 +420,40 @@ impl Repo { Err(eyre!("Swap commits failed: {}", stderr)) } + /// Resolved path to the git directory (supports `gitdir:` worktrees and `--separate-git-dir`). + fn git_directory(&self) -> PathBuf { + let abs = Command::new("git") + .current_dir(&self.workdir) + .args(["rev-parse", "--absolute-git-dir"]) + .output(); + if let Ok(o) = abs { + if o.status.success() { + let p = String::from_utf8_lossy(&o.stdout).trim().to_string(); + if !p.is_empty() { + return PathBuf::from(p); + } + } + } + let rel = Command::new("git") + .current_dir(&self.workdir) + .args(["rev-parse", "--git-dir"]) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_default(); + let p = PathBuf::from(&rel); + if p.is_absolute() { + p + } else { + self.workdir.join(p) + } + } + /// Check if a rebase is currently in progress. pub fn is_rebase_in_progress(&self) -> bool { - self.workdir.join(".git/rebase-merge").exists() - || self.workdir.join(".git/rebase-apply").exists() + let gd = self.git_directory(); + gd.join("rebase-merge").exists() || gd.join("rebase-apply").exists() } /// Get the list of files with conflicts (unmerged paths). @@ -622,14 +652,14 @@ impl Repo { /// Read sync state from .git/pgit-sync-state.json. pub fn read_sync_state(&self) -> std::collections::HashMap { - let path = self.workdir.join(".git/pgit-sync-state.json"); + let path = self.git_directory().join("pgit-sync-state.json"); let content = std::fs::read_to_string(&path).unwrap_or_default(); serde_json::from_str(&content).unwrap_or_default() } /// Write sync state to .git/pgit-sync-state.json. pub fn write_sync_state(&self, state: &std::collections::HashMap) { - let path = self.workdir.join(".git/pgit-sync-state.json"); + let path = self.git_directory().join("pgit-sync-state.json"); if let Ok(json) = serde_json::to_string_pretty(state) { let _ = std::fs::write(&path, json); } diff --git a/tests/test_git_ops.rs b/tests/test_git_ops.rs index faed391..bf53d12 100644 --- a/tests/test_git_ops.rs +++ b/tests/test_git_ops.rs @@ -1,11 +1,39 @@ use std::path::{Path, PathBuf}; use std::process::Command; +/// Root directory for ephemeral git repos during integration tests. +/// +/// Defaults to `target/pgit-integration-tmp/` under this crate so artifacts stay local and +/// ignored via `/target`. Override with `PGIT_TEST_TMP`. +/// +/// Repos are created with `git init --separate-git-dir`: some environments (e.g. Cursor's agent +/// sandbox) deny writes under `/.git/hooks` while still allowing a sibling git directory. +fn scratch_root() -> PathBuf { + if let Ok(p) = std::env::var("PGIT_TEST_TMP") { + return PathBuf::from(p); + } + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("target") + .join("pgit-integration-tmp") +} + +fn companion_git_dir(worktree: &Path) -> Option { + let name = worktree.file_name()?.to_str()?; + Some(worktree.parent()?.join(format!("{}.git", name))) +} + /// Helper: create a temp git repo with an initial commit on `main` /// and a local "origin" remote so detect_base works. fn setup_repo(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!("pgit-test-{}-{}", name, std::process::id())); + let root = scratch_root(); + std::fs::create_dir_all(&root).unwrap(); + let root = root.canonicalize().unwrap(); + + let key = format!("pgit-test-{}-{}", name, std::process::id()); + let dir = root.join(&key); + let git_dir = root.join(format!("{}.git", key)); let _ = std::fs::remove_dir_all(&dir); + let _ = std::fs::remove_dir_all(&git_dir); std::fs::create_dir_all(&dir).unwrap(); let git = |args: &[&str]| { @@ -23,9 +51,18 @@ fn setup_repo(name: &str) -> PathBuf { ); }; - git(&["init", "-b", "main"]); + git(&[ + "init", + "--separate-git-dir", + git_dir.to_str().expect("utf-8 temp path"), + "-b", + "main", + ]); git(&["config", "user.name", "Test User"]); git(&["config", "user.email", "test@example.com"]); + // Isolate from developer globals (gpgsign, etc.); sandboxed runs cannot use ~/.gnupg. + git(&["config", "commit.gpgsign", "false"]); + git(&["config", "tag.gpgSign", "false"]); // Initial commit on main std::fs::write(dir.join("README.md"), "# test\n").unwrap(); @@ -58,8 +95,11 @@ fn open_repo(dir: &Path) -> pilegit::git::ops::Repo { pilegit::git::ops::Repo::at_dir(dir.to_path_buf()) } -fn cleanup(dir: &PathBuf) { - let _ = std::fs::remove_dir_all(dir); +fn cleanup(worktree: &PathBuf) { + if let Some(git_dir) = companion_git_dir(worktree) { + let _ = std::fs::remove_dir_all(git_dir); + } + let _ = std::fs::remove_dir_all(worktree); } // --- Tests ---