Skip to content
Open
Show file tree
Hide file tree
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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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
38 changes: 34 additions & 4 deletions src/git/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -622,14 +652,14 @@ impl Repo {

/// Read sync state from .git/pgit-sync-state.json.
pub fn read_sync_state(&self) -> std::collections::HashMap<String, String> {
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<String, String>) {
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);
}
Expand Down
48 changes: 44 additions & 4 deletions tests/test_git_ops.rs
Original file line number Diff line number Diff line change
@@ -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 `<worktree>/.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<PathBuf> {
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]| {
Expand All @@ -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();
Expand Down Expand Up @@ -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 ---
Expand Down
Loading