Files
ElyOffice/apps/ely-tree/src/ops.rs
T
ZacharyZhang-NY d59159cea2 Fixes for M-02 to M-14, G-01 to G-14, K-02 to K-04, X-04 to X-14, not yet reviewed
Committed before review so Windows and WSL builds can start.
Each fix was tested and mutation-checked here; the codex review
of each item is still to come, so tasks/BUGS.md stays unticked.

M-02 is mid-review: round 1 findings are fixed (apps/ely-mark/src/tally.rs).
K-01 stays deferred. The ElyMark window test mark-outside.sh failed
once at its crash-recovery step in the last run; not yet looked into.
2026-10-10 02:29:19 -04:00

136 lines
4.0 KiB
Rust

//! A merge, rebase, cherry-pick or revert under way (TREE-08, §13.3).
use std::{io, path::Path};
use crate::git::Git;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Op {
Merge,
Rebase,
CherryPick,
Revert,
/// `git am`, applying mailed patches.
Am,
}
impl Op {
/// The Git command that continues, aborts or skips it.
pub fn verb(self) -> &'static str {
match self {
Op::Merge => "merge",
Op::Rebase => "rebase",
Op::CherryPick => "cherry-pick",
Op::Revert => "revert",
Op::Am => "am",
}
}
/// A merge has no step to skip.
pub fn skips(self) -> bool {
self != Op::Merge
}
}
/// The operation, and what its two sides stand for now.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Doing {
pub op: Op,
/// Git's "ours": what HEAD holds as the operation stands.
pub ours: String,
/// Git's "theirs": what is being brought in.
pub theirs: String,
}
/// Stopped on a conflict: Git said so and left some.
pub fn stopped(git: &Git, root: &Path, args: &[String], said: &str) -> bool {
let operation = matches!(
args.first().map(String::as_str),
Some("merge" | "rebase" | "cherry-pick" | "revert" | "pull")
);
if !operation || !said.lines().any(|line| line.starts_with("CONFLICT (")) {
return false;
}
match git.run(root, &["ls-files", "--unmerged", "-z"]) {
Ok(unmerged) => !unmerged.is_empty(),
Err(error) => {
log::error!("tree: unmerged entries unread: {error}");
false
}
}
}
/// A commit as `abc1234 subject`.
fn describe(git: &Git, root: &Path, rev: &str) -> io::Result<String> {
let said = git.run(root, &["log", "-1", "--format=%h %s", rev, "--"])?;
Ok(String::from_utf8_lossy(&said).trim().to_string())
}
/// The branch HEAD is on, or its commit when detached.
fn head(git: &Git, root: &Path) -> io::Result<String> {
let branch = git.run_or_one(root, &["symbolic-ref", "--short", "-q", "HEAD"])?;
match String::from_utf8_lossy(&branch).trim() {
"" => describe(git, root, "HEAD"),
branch => Ok(branch.to_string()),
}
}
/// What is under way in the work tree of `git_dir`.
pub fn doing(git: &Git, root: &Path, git_dir: &Path) -> io::Result<Option<Doing>> {
let has = |name: &str| git_dir.join(name).exists();
let read = |name: &str| {
std::fs::read_to_string(git_dir.join(name)).map(|text| text.trim().to_string())
};
// `git am` keeps rebase-apply too, without an onto.
let doing = if has("rebase-apply/applying") {
Doing {
op: Op::Am,
ours: head(git, root)?,
theirs: format!(
"{}/{}",
read("rebase-apply/next")?,
read("rebase-apply/last")?
),
}
} else if has("rebase-merge") || has("rebase-apply") {
let dir = if has("rebase-merge") {
"rebase-merge"
} else {
"rebase-apply"
};
// In a rebase, ours is the base; theirs, the replay.
let onto = describe(git, root, &read(&format!("{dir}/onto"))?)?;
let theirs = if has("REBASE_HEAD") {
describe(git, root, "REBASE_HEAD")?
} else {
String::new()
};
Doing {
op: Op::Rebase,
ours: onto,
theirs,
}
} else if has("MERGE_HEAD") {
Doing {
op: Op::Merge,
ours: head(git, root)?,
theirs: describe(git, root, "MERGE_HEAD")?,
}
} else if has("CHERRY_PICK_HEAD") {
Doing {
op: Op::CherryPick,
ours: head(git, root)?,
theirs: describe(git, root, "CHERRY_PICK_HEAD")?,
}
} else if has("REVERT_HEAD") {
Doing {
op: Op::Revert,
ours: head(git, root)?,
theirs: describe(git, root, "REVERT_HEAD")?,
}
} else {
return Ok(None);
};
Ok(Some(doing))
}