🐞 fix: Even more git fixes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user