#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
use base64::Engine;
use git_sshripped_worktree_models::UnlockSession;
fn git_rev_parse(cwd: &Path, arg: &str) -> Result<PathBuf> {
profiling::scope!("git rev-parse", arg);
let output = Command::new("git")
.args(["rev-parse", arg])
.current_dir(cwd)
.output()
.with_context(|| format!("failed to execute git rev-parse {arg}"))?;
if !output.status.success() {
anyhow::bail!(
"git rev-parse {arg} failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
let text = String::from_utf8(output.stdout).context("git rev-parse output was not utf8")?;
Ok(PathBuf::from(text.trim()))
}
pub fn git_common_dir(cwd: &Path) -> Result<PathBuf> {
profiling::scope!("git_common_dir");
git_rev_parse(cwd, "--git-common-dir")
}
pub fn git_dir(cwd: &Path) -> Result<PathBuf> {
profiling::scope!("git_dir");
git_rev_parse(cwd, "--git-dir")
}
pub fn git_toplevel(cwd: &Path) -> Result<PathBuf> {
profiling::scope!("git_toplevel");
git_rev_parse(cwd, "--show-toplevel")
}
pub fn is_linked_worktree(cwd: &Path) -> Result<bool> {
profiling::scope!("is_linked_worktree");
let git_dir_raw = git_dir(cwd)?;
let common_dir_raw = git_common_dir(cwd)?;
Ok(is_linked_worktree_inner(cwd, &git_dir_raw, &common_dir_raw))
}
#[derive(Debug, Clone)]
pub struct WorktreeIdentity {
pub git_dir: PathBuf,
pub linked: bool,
}
pub fn resolve_worktree_identity(cwd: &Path, common_dir: &Path) -> Result<WorktreeIdentity> {
profiling::scope!("resolve_worktree_identity");
let git_dir_raw = git_dir(cwd)?;
let abs_git = if git_dir_raw.is_absolute() {
git_dir_raw
} else {
cwd.join(git_dir_raw)
};
let linked = is_linked_worktree_inner(cwd, &abs_git, common_dir);
Ok(WorktreeIdentity {
git_dir: abs_git,
linked,
})
}
fn is_linked_worktree_inner(cwd: &Path, git_dir_raw: &Path, common_dir_raw: &Path) -> bool {
let abs_git = if git_dir_raw.is_absolute() {
git_dir_raw.to_path_buf()
} else {
cwd.join(git_dir_raw)
};
let abs_common = if common_dir_raw.is_absolute() {
common_dir_raw.to_path_buf()
} else {
cwd.join(common_dir_raw)
};
let canon_git = fs::canonicalize(&abs_git).unwrap_or(abs_git);
let canon_common = fs::canonicalize(&abs_common).unwrap_or(abs_common);
canon_git != canon_common
}
#[must_use]
pub fn session_file(common_dir: &Path) -> PathBuf {
common_dir
.join("git-sshripped")
.join("session")
.join("unlock.json")
}
pub fn write_unlock_session(
common_dir: &Path,
key: &[u8],
key_source: &str,
repo_key_id: Option<String>,
) -> Result<()> {
profiling::scope!("write_unlock_session");
let file = session_file(common_dir);
let parent = file
.parent()
.context("session path has no parent directory")?;
fs::create_dir_all(parent)
.with_context(|| format!("failed to create session dir {}", parent.display()))?;
let session = UnlockSession {
key_b64: base64::engine::general_purpose::STANDARD_NO_PAD.encode(key),
key_source: key_source.to_string(),
repo_key_id,
};
let text =
serde_json::to_string_pretty(&session).context("failed to serialize unlock session")?;
fs::write(&file, text)
.with_context(|| format!("failed to write session file {}", file.display()))?;
#[cfg(unix)]
{
let mut perms = fs::metadata(&file)
.with_context(|| format!("failed to read session file metadata {}", file.display()))?
.permissions();
perms.set_mode(0o600);
fs::set_permissions(&file, perms)
.with_context(|| format!("failed to set secure permissions on {}", file.display()))?;
}
Ok(())
}
pub fn clear_unlock_session(common_dir: &Path) -> Result<()> {
profiling::scope!("clear_unlock_session");
let file = session_file(common_dir);
if file.exists() {
fs::remove_file(&file)
.with_context(|| format!("failed to remove session file {}", file.display()))?;
}
Ok(())
}
pub fn read_unlock_session(common_dir: &Path) -> Result<Option<UnlockSession>> {
profiling::scope!("read_unlock_session");
let file = session_file(common_dir);
if !file.exists() {
return Ok(None);
}
let text = fs::read_to_string(&file)
.with_context(|| format!("failed to read session file {}", file.display()))?;
let session = serde_json::from_str(&text)
.with_context(|| format!("failed to parse session file {}", file.display()))?;
Ok(Some(session))
}