f473b63ee3
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>
107 lines
3.6 KiB
Rust
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())
|
|
}
|