🐞 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/notes/assets/.gitkeep
|
||||||
data/projects/*/
|
data/projects/*/
|
||||||
!data/projects/.gitkeep
|
!data/projects/.gitkeep
|
||||||
|
Codex Session.txt
|
||||||
|
|
||||||
# === Stray root lock file (frontend/package-lock.json is kept for CI) ===
|
# === Stray root lock file (frontend/package-lock.json is kept for CI) ===
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ FROM debian:bookworm-slim
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update \
|
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/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=backend-builder /src/backend/target/release/ironpad /app/ironpad
|
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:
|
3. Set:
|
||||||
- `IRONPAD_GIT_REMOTE_URL` (example: `git@github.com:your-org/ironpad-data.git`)
|
- `IRONPAD_GIT_REMOTE_URL` (example: `git@github.com:your-org/ironpad-data.git`)
|
||||||
- `IRONPAD_GIT_SSH_PRIVATE_KEY` (path inside container)
|
- `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`)
|
- `IRONPAD_GIT_SYNC_INTERVAL_SECS` (example: `300`)
|
||||||
4. Recreate the stack:
|
4. Recreate the stack:
|
||||||
|
|
||||||
@@ -112,6 +113,7 @@ docker compose up -d --build
|
|||||||
Sync behavior:
|
Sync behavior:
|
||||||
- Every cycle: `fetch -> safe fast-forward if possible -> push`
|
- 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 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
|
## 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 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 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 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 username | `git` | Override with `IRONPAD_GIT_SSH_USERNAME` if needed |
|
||||||
| Git SSH passphrase | not set | Optional `IRONPAD_GIT_SSH_PASSPHRASE` |
|
| 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 |
|
| Auto-save | 1 second debounce | Frontend saves after typing stops |
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@@ -196,6 +197,72 @@ fn remote_callbacks() -> git2::RemoteCallbacks<'static> {
|
|||||||
callbacks
|
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
|
/// Get repository status
|
||||||
pub fn get_status() -> Result<RepoStatus, String> {
|
pub fn get_status() -> Result<RepoStatus, String> {
|
||||||
let data_path = config::data_dir();
|
let data_path = config::data_dir();
|
||||||
@@ -423,12 +490,27 @@ pub fn push_to_remote() -> Result<(), String> {
|
|||||||
|
|
||||||
// Push the current branch
|
// Push the current branch
|
||||||
let refspec = format!("refs/heads/{}:refs/heads/{}", branch_name, branch_name);
|
let refspec = format!("refs/heads/{}:refs/heads/{}", branch_name, branch_name);
|
||||||
remote
|
match remote.push(&[&refspec], Some(&mut push_options)) {
|
||||||
.push(&[&refspec], Some(&mut push_options))
|
Ok(_) => {
|
||||||
.map_err(|e| format!("Push failed: {}. Make sure SSH keys are configured.", e))?;
|
tracing::info!("Successfully pushed to origin/{}", branch_name);
|
||||||
|
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
|
/// Check if remote is configured
|
||||||
@@ -850,18 +932,20 @@ pub fn fetch_from_remote() -> Result<(), String> {
|
|||||||
.find_remote("origin")
|
.find_remote("origin")
|
||||||
.map_err(|e| format!("Remote 'origin' not found: {}", e))?;
|
.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();
|
let mut fetch_options = git2::FetchOptions::new();
|
||||||
fetch_options.remote_callbacks(callbacks);
|
fetch_options.remote_callbacks(remote_callbacks());
|
||||||
|
|
||||||
remote
|
match remote.fetch(&[] as &[&str], Some(&mut fetch_options), None) {
|
||||||
.fetch(&[] as &[&str], Some(&mut fetch_options), None)
|
Ok(_) => Ok(()),
|
||||||
.map_err(|e| format!("Fetch failed: {}", e))?;
|
Err(e) => {
|
||||||
|
let err_text = e.to_string();
|
||||||
Ok(())
|
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_USERNAME: "git"
|
||||||
# IRONPAD_GIT_SSH_PRIVATE_KEY: "/run/secrets/ironpad_ssh/id_ed25519"
|
# 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_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_SSH_PASSPHRASE: ""
|
||||||
# IRONPAD_GIT_SYNC_INTERVAL_SECS: "300"
|
# IRONPAD_GIT_SYNC_INTERVAL_SECS: "300"
|
||||||
|
# IRONPAD_GIT_USE_CLI_FALLBACK: "true"
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
|
|||||||
Reference in New Issue
Block a user