Skip to main content

jj_hooks/
worktree.rs

1//! Ephemeral git worktree used to run hooks at a target commit without
2//! disturbing the user's working copy or polluting the shared `.git/index`.
3//!
4//! Created via `git worktree add --detach`, removed on drop.
5
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9use tempfile::TempDir;
10use tracing::warn;
11
12use crate::error::{JjHooksError, Result};
13
14pub struct Worktree {
15    git_dir: PathBuf,
16    dir: TempDir,
17    removed: bool,
18}
19
20impl Worktree {
21    /// Create a detached worktree at `commit` using the given primary git dir.
22    pub fn create(git_dir: &Path, commit: &str) -> Result<Self> {
23        let dir = TempDir::with_prefix("jj-hooks-worktree-")?;
24
25        let output = Command::new("git")
26            .arg(format!("--git-dir={}", git_dir.display()))
27            .args(["worktree", "add", "--detach", "--quiet"])
28            .arg(dir.path())
29            .arg(commit)
30            .output()?;
31
32        if !output.status.success() {
33            return Err(JjHooksError::JjFailed {
34                status: output.status.code().unwrap_or(-1),
35                stderr: format!(
36                    "git worktree add failed: {}",
37                    String::from_utf8_lossy(&output.stderr)
38                ),
39            });
40        }
41
42        Ok(Self {
43            git_dir: git_dir.to_owned(),
44            dir,
45            removed: false,
46        })
47    }
48
49    pub fn path(&self) -> &Path {
50        self.dir.path()
51    }
52
53    pub fn git_dir(&self) -> &Path {
54        &self.git_dir
55    }
56
57    fn remove(&mut self) -> std::io::Result<()> {
58        if self.removed {
59            return Ok(());
60        }
61        let output = Command::new("git")
62            .arg(format!("--git-dir={}", self.git_dir.display()))
63            .args(["worktree", "remove", "--force"])
64            .arg(self.dir.path())
65            .output()?;
66
67        if !output.status.success() {
68            return Err(std::io::Error::other(format!(
69                "git worktree remove failed: {}",
70                String::from_utf8_lossy(&output.stderr)
71            )));
72        }
73
74        self.removed = true;
75        Ok(())
76    }
77}
78
79impl Drop for Worktree {
80    fn drop(&mut self) {
81        if let Err(e) = self.remove() {
82            warn!(
83                "failed to clean up worktree at {}: {e}",
84                self.dir.path().display()
85            );
86        }
87    }
88}