🐞 fix: Even more git fixes
Some checks failed
CI / Backend (Rust) (push) Failing after 9s
CI / Frontend (Vue) (push) Successful in 9m57s

This commit is contained in:
Keith Solomon
2026-02-25 08:05:02 -06:00
parent 2a55d5ebec
commit f1f106c948
5 changed files with 110 additions and 19 deletions

1
.gitignore vendored
View File

@@ -37,6 +37,7 @@ data/notes/assets/*
!data/notes/assets/.gitkeep
data/projects/*/
!data/projects/.gitkeep
Codex Session.txt
# === Stray root lock file (frontend/package-lock.json is kept for CI) ===
/package-lock.json

View File

@@ -21,7 +21,7 @@ FROM debian:bookworm-slim
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates ripgrep libdbus-1-3 \
&& apt-get install -y --no-install-recommends ca-certificates ripgrep libdbus-1-3 git openssh-client \
&& rm -rf /var/lib/apt/lists/*
COPY --from=backend-builder /src/backend/target/release/ironpad /app/ironpad

View File

@@ -102,6 +102,7 @@ Ironpad can automatically sync the `data/` git repo with a private remote over S
3. Set:
- `IRONPAD_GIT_REMOTE_URL` (example: `git@github.com:your-org/ironpad-data.git`)
- `IRONPAD_GIT_SSH_PRIVATE_KEY` (path inside container)
- `IRONPAD_GIT_SSH_KNOWN_HOSTS` (optional; defaults to `/root/.ssh/known_hosts`)
- `IRONPAD_GIT_SYNC_INTERVAL_SECS` (example: `300`)
4. Recreate the stack:
@@ -112,6 +113,7 @@ docker compose up -d --build
Sync behavior:
- Every cycle: `fetch -> safe fast-forward if possible -> push`
- If local and remote diverge, auto fast-forward is skipped and a warning is logged.
- If libgit2 SSH auth fails, Ironpad can fall back to `git` CLI (controlled by `IRONPAD_GIT_USE_CLI_FALLBACK`, default `true`).
## Tech Stack
@@ -174,8 +176,10 @@ Read about the method:
| Git sync interval | `0` (disabled) | Set `IRONPAD_GIT_SYNC_INTERVAL_SECS` to enable scheduled sync |
| Git SSH private key | not set | `IRONPAD_GIT_SSH_PRIVATE_KEY` path to private key in container |
| Git SSH public key | not set | Optional `IRONPAD_GIT_SSH_PUBLIC_KEY` path |
| Git known_hosts path | `/root/.ssh/known_hosts` | Override with `IRONPAD_GIT_SSH_KNOWN_HOSTS` |
| Git SSH username | `git` | Override with `IRONPAD_GIT_SSH_USERNAME` if needed |
| Git SSH passphrase | not set | Optional `IRONPAD_GIT_SSH_PASSPHRASE` |
| Git CLI fallback | `true` | `IRONPAD_GIT_USE_CLI_FALLBACK` for fetch/push auth fallback |
| Auto-save | 1 second debounce | Frontend saves after typing stops |
## Documentation

View File

@@ -1,4 +1,5 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use chrono::Utc;
@@ -196,6 +197,72 @@ fn remote_callbacks() -> git2::RemoteCallbacks<'static> {
callbacks
}
fn use_git_cli_fallback() -> bool {
std::env::var("IRONPAD_GIT_USE_CLI_FALLBACK")
.map(|v| !matches!(v.to_ascii_lowercase().as_str(), "0" | "false" | "no" | "off"))
.unwrap_or(true)
}
fn known_hosts_path() -> Option<PathBuf> {
if let Ok(path) = std::env::var("IRONPAD_GIT_SSH_KNOWN_HOSTS") {
let p = PathBuf::from(path);
if p.exists() {
return Some(p);
}
}
let default = PathBuf::from("/root/.ssh/known_hosts");
if default.exists() {
Some(default)
} else {
None
}
}
fn git_ssh_command(auth: &GitAuthConfig) -> Option<String> {
let private_key = auth.private_key.as_ref()?;
let mut cmd = format!(
"ssh -i {} -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes",
private_key.display()
);
if let Some(known_hosts) = known_hosts_path() {
cmd.push_str(&format!(" -o UserKnownHostsFile={}", known_hosts.display()));
}
Some(cmd)
}
fn run_git_cli(args: &[&str]) -> Result<(), String> {
let auth = git_auth_config();
let data_path = config::data_dir();
let mut cmd = Command::new("git");
cmd.args(args)
.current_dir(data_path)
.env("GIT_TERMINAL_PROMPT", "0");
if let Some(ssh_cmd) = git_ssh_command(&auth) {
cmd.env("GIT_SSH_COMMAND", ssh_cmd);
}
let output = cmd
.output()
.map_err(|e| format!("Failed to run git CLI: {}", e))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let msg = if !stderr.is_empty() { stderr } else { stdout };
Err(format!(
"git {} failed (exit {}): {}",
args.join(" "),
output.status.code().unwrap_or(-1),
msg
))
}
}
/// Get repository status
pub fn get_status() -> Result<RepoStatus, String> {
let data_path = config::data_dir();
@@ -423,12 +490,27 @@ pub fn push_to_remote() -> Result<(), String> {
// Push the current branch
let refspec = format!("refs/heads/{}:refs/heads/{}", branch_name, branch_name);
remote
.push(&[&refspec], Some(&mut push_options))
.map_err(|e| format!("Push failed: {}. Make sure SSH keys are configured.", e))?;
tracing::info!("Successfully pushed to origin/{}", branch_name);
Ok(())
match remote.push(&[&refspec], Some(&mut push_options)) {
Ok(_) => {
tracing::info!("Successfully pushed to origin/{}", branch_name);
Ok(())
}
Err(e) => {
let err_text = e.to_string();
if !use_git_cli_fallback() {
return Err(format!(
"Push failed: {}. Make sure SSH keys are configured.",
err_text
));
}
tracing::warn!("libgit2 push failed, trying git CLI fallback: {}", err_text);
run_git_cli(&["push", "origin", branch_name]).map_err(|cli_err| {
format!("Push failed: {} (libgit2) / {} (git CLI)", err_text, cli_err)
})?;
tracing::info!("Successfully pushed to origin/{} (git CLI fallback)", branch_name);
Ok(())
}
}
}
/// Check if remote is configured
@@ -850,18 +932,20 @@ pub fn fetch_from_remote() -> Result<(), String> {
.find_remote("origin")
.map_err(|e| format!("Remote 'origin' not found: {}", e))?;
// Create callbacks for authentication
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.credentials(|_url, username_from_url, _allowed_types| {
git2::Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
});
let mut fetch_options = git2::FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
fetch_options.remote_callbacks(remote_callbacks());
remote
.fetch(&[] as &[&str], Some(&mut fetch_options), None)
.map_err(|e| format!("Fetch failed: {}", e))?;
Ok(())
match remote.fetch(&[] as &[&str], Some(&mut fetch_options), None) {
Ok(_) => Ok(()),
Err(e) => {
let err_text = e.to_string();
if !use_git_cli_fallback() {
return Err(format!("Fetch failed: {}", err_text));
}
tracing::warn!("libgit2 fetch failed, trying git CLI fallback: {}", err_text);
run_git_cli(&["fetch", "origin", "--prune"]).map_err(|cli_err| {
format!("Fetch failed: {} (libgit2) / {} (git CLI)", err_text, cli_err)
})
}
}
}

View File

@@ -18,8 +18,10 @@ services:
# IRONPAD_GIT_SSH_USERNAME: "git"
# IRONPAD_GIT_SSH_PRIVATE_KEY: "/run/secrets/ironpad_ssh/id_ed25519"
# IRONPAD_GIT_SSH_PUBLIC_KEY: "/run/secrets/ironpad_ssh/id_ed25519.pub"
# IRONPAD_GIT_SSH_KNOWN_HOSTS: "/run/secrets/ironpad_ssh/known_hosts"
# IRONPAD_GIT_SSH_PASSPHRASE: ""
# IRONPAD_GIT_SYNC_INTERVAL_SECS: "300"
# IRONPAD_GIT_USE_CLI_FALLBACK: "true"
ports:
- "3000:3000"