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> {
39 git_rev_parse(cwd, "--git-common-dir")
40}
41
42pub 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
59pub 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
101pub 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
115pub 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}