Skip to main content

git_sshripped_worktree/
lib.rs

1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
3#![allow(clippy::multiple_crate_versions)]
4
5use std::fs;
6#[cfg(unix)]
7use std::os::unix::fs::PermissionsExt;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11use anyhow::{Context, Result};
12use base64::Engine;
13use git_sshripped_worktree_models::UnlockSession;
14
15fn git_rev_parse(cwd: &Path, arg: &str) -> Result<PathBuf> {
16    let output = Command::new("git")
17        .args(["rev-parse", arg])
18        .current_dir(cwd)
19        .output()
20        .with_context(|| format!("failed to execute git rev-parse {arg}"))?;
21
22    if !output.status.success() {
23        anyhow::bail!(
24            "git rev-parse {arg} failed: {}",
25            String::from_utf8_lossy(&output.stderr).trim()
26        );
27    }
28
29    let text = String::from_utf8(output.stdout).context("git rev-parse output was not utf8")?;
30    Ok(PathBuf::from(text.trim()))
31}
32
33/// Resolve the Git common directory for the current working directory.
34///
35/// # Errors
36///
37/// Returns an error if `git rev-parse --git-common-dir` fails.
38pub fn git_common_dir(cwd: &Path) -> Result<PathBuf> {
39    git_rev_parse(cwd, "--git-common-dir")
40}
41
42/// Resolve the working-tree root for the current working directory.
43///
44/// # Errors
45///
46/// Returns an error if `git rev-parse --show-toplevel` fails.
47pub fn git_toplevel(cwd: &Path) -> Result<PathBuf> {
48    git_rev_parse(cwd, "--show-toplevel")
49}
50
51#[must_use]
52pub fn session_file(common_dir: &Path) -> PathBuf {
53    common_dir
54        .join("git-sshripped")
55        .join("session")
56        .join("unlock.json")
57}
58
59/// Persist the unlock session to disk.
60///
61/// # Errors
62///
63/// Returns an error if the session directory cannot be created, the session
64/// cannot be serialized, or the file cannot be written.
65pub fn write_unlock_session(
66    common_dir: &Path,
67    key: &[u8],
68    key_source: &str,
69    repo_key_id: Option<String>,
70) -> Result<()> {
71    let file = session_file(common_dir);
72    let parent = file
73        .parent()
74        .context("session path has no parent directory")?;
75    fs::create_dir_all(parent)
76        .with_context(|| format!("failed to create session dir {}", parent.display()))?;
77
78    let session = UnlockSession {
79        key_b64: base64::engine::general_purpose::STANDARD_NO_PAD.encode(key),
80        key_source: key_source.to_string(),
81        repo_key_id,
82    };
83    let text =
84        serde_json::to_string_pretty(&session).context("failed to serialize unlock session")?;
85    fs::write(&file, text)
86        .with_context(|| format!("failed to write session file {}", file.display()))?;
87
88    #[cfg(unix)]
89    {
90        let mut perms = fs::metadata(&file)
91            .with_context(|| format!("failed to read session file metadata {}", file.display()))?
92            .permissions();
93        perms.set_mode(0o600);
94        fs::set_permissions(&file, perms)
95            .with_context(|| format!("failed to set secure permissions on {}", file.display()))?;
96    }
97
98    Ok(())
99}
100
101/// Remove the unlock session file, locking the repository.
102///
103/// # Errors
104///
105/// Returns an error if the session file exists but cannot be removed.
106pub fn clear_unlock_session(common_dir: &Path) -> Result<()> {
107    let file = session_file(common_dir);
108    if file.exists() {
109        fs::remove_file(&file)
110            .with_context(|| format!("failed to remove session file {}", file.display()))?;
111    }
112    Ok(())
113}
114
115/// Read the current unlock session, if one exists.
116///
117/// # Errors
118///
119/// Returns an error if the session file exists but cannot be read or parsed.
120pub fn read_unlock_session(common_dir: &Path) -> Result<Option<UnlockSession>> {
121    let file = session_file(common_dir);
122    if !file.exists() {
123        return Ok(None);
124    }
125    let text = fs::read_to_string(&file)
126        .with_context(|| format!("failed to read session file {}", file.display()))?;
127    let session = serde_json::from_str(&text)
128        .with_context(|| format!("failed to parse session file {}", file.display()))?;
129    Ok(Some(session))
130}