git_sshripped_worktree/
lib.rs1#![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}