Files
md-manuscript/src/gitsync.rs
T
landon f473b63ee3 Initial commit: md-manuscript editor
Rust/egui desktop app: draggable markdown file list, editor, git sync,
per-file chapter titles, and native ODT export. Includes README and
Debian 12 (Surface) install guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 15:17:42 -05:00

107 lines
3.6 KiB
Rust

use std::path::Path;
use std::process::Command;
/// Result of a git operation: combined human-readable log for display.
pub struct GitOutcome {
pub ok: bool,
pub log: String,
}
fn run_git(workspace: &Path, args: &[&str]) -> (bool, String) {
let output = Command::new("git")
.current_dir(workspace)
.args(args)
.output();
match output {
Ok(out) => {
let mut text = String::new();
text.push_str(&format!("$ git {}\n", args.join(" ")));
text.push_str(&String::from_utf8_lossy(&out.stdout));
let err = String::from_utf8_lossy(&out.stderr);
if !err.trim().is_empty() {
text.push_str(&err);
}
(out.status.success(), text)
}
Err(e) => (false, format!("failed to launch git: {e}\n")),
}
}
/// True if the workspace directory is inside a git working tree.
pub fn is_repo(workspace: &Path) -> bool {
let (ok, out) = run_git(workspace, &["rev-parse", "--is-inside-work-tree"]);
ok && out.contains("true")
}
/// True if a remote named `origin` is configured.
fn has_origin(workspace: &Path) -> bool {
let (ok, out) = run_git(workspace, &["remote"]);
ok && out.lines().any(|l| l.trim() == "origin")
}
/// Initialise a new git repository in the workspace with a starter .gitignore.
pub fn init(workspace: &Path) -> GitOutcome {
let mut log = String::new();
let (ok1, o1) = run_git(workspace, &["init"]);
log.push_str(&o1);
// Make sure we are on a predictable default branch name.
let (_, o2) = run_git(workspace, &["symbolic-ref", "HEAD", "refs/heads/main"]);
log.push_str(&o2);
GitOutcome { ok: ok1, log }
}
/// Commit all local changes and, if `origin` exists, pull (rebase) then push.
/// This is the one-button "sync" operation.
pub fn sync(workspace: &Path, message: &str) -> GitOutcome {
let mut log = String::new();
let mut ok = true;
let (_, o) = run_git(workspace, &["add", "-A"]);
log.push_str(&o);
// Commit only if there is something staged; otherwise git returns non-zero,
// which is fine and should not abort the sync.
let (has_changes, status) = run_git(workspace, &["status", "--porcelain"]);
let dirty = has_changes && !status.lines().skip(1).all(|l| l.trim().is_empty());
if dirty {
let (c_ok, c_out) = run_git(workspace, &["commit", "-m", message]);
log.push_str(&c_out);
ok &= c_ok;
} else {
log.push_str("(nothing to commit)\n");
}
if has_origin(workspace) {
let (p_ok, p_out) = run_git(workspace, &["pull", "--rebase", "--autostash"]);
log.push_str(&p_out);
ok &= p_ok;
let (push_ok, push_out) = run_git(workspace, &["push"]);
log.push_str(&push_out);
// A failed push often just means no upstream is set yet; surface it but
// do not treat a missing upstream as a hard failure of the whole sync.
if !push_ok && push_out.contains("no upstream") {
let branch = current_branch(workspace);
let (u_ok, u_out) =
run_git(workspace, &["push", "--set-upstream", "origin", &branch]);
log.push_str(&u_out);
ok &= u_ok;
} else {
ok &= push_ok;
}
} else {
log.push_str("(no 'origin' remote configured — committed locally only)\n");
}
GitOutcome { ok, log }
}
fn current_branch(workspace: &Path) -> String {
let (_, out) = run_git(workspace, &["rev-parse", "--abbrev-ref", "HEAD"]);
out.lines()
.last()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "main".to_string())
}