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
33pub fn git_common_dir(cwd: &Path) -> Result<PathBuf> {
34    git_rev_parse(cwd, "--git-common-dir")
35}
36
37pub fn git_toplevel(cwd: &Path) -> Result<PathBuf> {
38    git_rev_parse(cwd, "--show-toplevel")
39}
40
41#[must_use]
42pub fn session_file(common_dir: &Path) -> PathBuf {
43    common_dir
44        .join("git-sshripped")
45        .join("session")
46        .join("unlock.json")
47}
48
49pub fn write_unlock_session(
50    common_dir: &Path,
51    key: &[u8],
52    key_source: &str,
53    repo_key_id: Option<String>,
54) -> Result<()> {
55    let file = session_file(common_dir);
56    let parent = file
57        .parent()
58        .context("session path has no parent directory")?;
59    fs::create_dir_all(parent)
60        .with_context(|| format!("failed to create session dir {}", parent.display()))?;
61
62    let session = UnlockSession {
63        key_b64: base64::engine::general_purpose::STANDARD_NO_PAD.encode(key),
64        key_source: key_source.to_string(),
65        repo_key_id,
66    };
67    let text =
68        serde_json::to_string_pretty(&session).context("failed to serialize unlock session")?;
69    fs::write(&file, text)
70        .with_context(|| format!("failed to write session file {}", file.display()))?;
71
72    #[cfg(unix)]
73    {
74        let mut perms = fs::metadata(&file)
75            .with_context(|| format!("failed to read session file metadata {}", file.display()))?
76            .permissions();
77        perms.set_mode(0o600);
78        fs::set_permissions(&file, perms)
79            .with_context(|| format!("failed to set secure permissions on {}", file.display()))?;
80    }
81
82    Ok(())
83}
84
85pub fn clear_unlock_session(common_dir: &Path) -> Result<()> {
86    let file = session_file(common_dir);
87    if file.exists() {
88        fs::remove_file(&file)
89            .with_context(|| format!("failed to remove session file {}", file.display()))?;
90    }
91    Ok(())
92}
93
94pub fn read_unlock_session(common_dir: &Path) -> Result<Option<UnlockSession>> {
95    let file = session_file(common_dir);
96    if !file.exists() {
97        return Ok(None);
98    }
99    let text = fs::read_to_string(&file)
100        .with_context(|| format!("failed to read session file {}", file.display()))?;
101    let session = serde_json::from_str(&text)
102        .with_context(|| format!("failed to parse session file {}", file.display()))?;
103    Ok(Some(session))
104}