use std::collections::HashSet;
use std::path::Path;
use anyhow::{Context, Result, bail};
use git2::{Oid, Repository};
use crate::core::msg;
use crate::core::repo;
use crate::git;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
Pick,
Edit,
Fixup,
}
impl Command {
fn as_str(&self) -> &str {
match self {
Command::Pick => "pick",
Command::Edit => "edit",
Command::Fixup => "fixup",
}
}
}
#[derive(Debug, Clone)]
pub struct CommitEntry {
pub oid: Oid,
pub short_hash: String,
pub message: String,
pub command: Command,
pub update_refs: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct BranchSection {
pub reset_target: String,
pub commits: Vec<CommitEntry>,
pub label: String,
pub branch_names: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum IntegrationEntry {
Pick(CommitEntry),
Merge {
original_oid: Option<Oid>,
label: String,
},
}
#[derive(Debug, Clone)]
pub struct Weave {
pub base_oid: Oid,
pub branch_sections: Vec<BranchSection>,
pub integration_line: Vec<IntegrationEntry>,
}
impl Weave {
pub fn to_todo(&self) -> String {
let mut out = String::new();
out.push_str("label onto\n");
for section in &self.branch_sections {
out.push('\n');
out.push_str(&format!("reset {}\n", section.reset_target));
let remaining = emit_commits_with_refs(&mut out, §ion.commits);
flush_refs(&mut out, &remaining);
out.push_str(&format!("label {}\n", section.label));
for branch_name in §ion.branch_names {
out.push_str(&format!("update-ref refs/heads/{}\n", branch_name));
}
}
out.push('\n');
out.push_str("reset onto\n");
let mut pending_refs: Vec<String> = Vec::new();
for entry in &self.integration_line {
match entry {
IntegrationEntry::Pick(commit) => {
if commit.command != Command::Fixup && !pending_refs.is_empty() {
flush_refs(&mut out, &pending_refs);
pending_refs.clear();
}
out.push_str(&format!(
"{} {} # {}\n",
commit.command.as_str(),
commit.short_hash,
commit.message
));
pending_refs.extend(commit.update_refs.iter().cloned());
}
IntegrationEntry::Merge {
original_oid,
label,
} => {
flush_refs(&mut out, &pending_refs);
pending_refs.clear();
if let Some(oid) = original_oid {
out.push_str(&format!(
"merge -C {} {} # Merge branch '{}'\n",
git::short_hash(&oid.to_string()),
label,
label
));
} else {
out.push_str(&format!("merge {} # Merge branch '{}'\n", label, label));
}
}
}
}
flush_refs(&mut out, &pending_refs);
out
}
pub fn from_repo(repo: &Repository) -> Result<Self> {
let info = repo::gather_repo_info(repo, false, 1)?;
Self::from_repo_with_info(repo, &info)
}
pub fn from_repo_with_info(repo: &Repository, info: &repo::RepoInfo) -> Result<Self> {
let head_oid = repo::head_oid(repo)?;
let merge_base_oid = info.upstream.merge_base_oid;
let first_parent_entries = walk_first_parent_line(repo, head_oid, merge_base_oid)?;
let branch_tips: std::collections::HashMap<Oid, Vec<String>> = {
let mut map: std::collections::HashMap<Oid, Vec<String>> =
std::collections::HashMap::new();
for branch in &info.branches {
map.entry(branch.tip_oid)
.or_default()
.push(branch.name.clone());
}
map
};
let mut branch_sections = Vec::new();
let mut integration_line = Vec::new();
let mut assigned_branches: std::collections::HashSet<String> =
std::collections::HashSet::new();
for entry in &first_parent_entries {
if entry.is_merge {
if let Some(merge_parent_oid) = entry.merge_parent {
let branch_names_at_tip = branch_tips
.get(&merge_parent_oid)
.cloned()
.unwrap_or_default();
let branch_commits =
walk_branch_commits(repo, merge_parent_oid, merge_base_oid)?;
if !branch_commits.is_empty() || !branch_names_at_tip.is_empty() {
let label = if !branch_names_at_tip.is_empty() {
branch_names_at_tip[0].clone()
} else {
format!("section-{}", git::short_hash(&merge_parent_oid.to_string()))
};
let todo_commits: Vec<CommitEntry> = branch_commits
.into_iter()
.rev()
.map(|c| {
let mut update_refs = Vec::new();
if let Some(names) = branch_tips.get(&c.oid) {
for name in names {
if !branch_names_at_tip.contains(name)
&& !assigned_branches.contains(name)
{
update_refs.push(name.clone());
}
}
}
CommitEntry {
oid: c.oid,
short_hash: c.short_hash,
message: c.message,
command: Command::Pick,
update_refs,
}
})
.collect();
let section = BranchSection {
reset_target: "onto".to_string(),
commits: todo_commits,
label: label.clone(),
branch_names: branch_names_at_tip.clone(),
};
for name in &branch_names_at_tip {
assigned_branches.insert(name.clone());
}
branch_sections.push(section);
integration_line.push(IntegrationEntry::Merge {
original_oid: Some(entry.oid),
label,
});
}
}
} else {
let mut update_refs = Vec::new();
if let Some(names) = branch_tips.get(&entry.oid) {
for name in names {
if !assigned_branches.contains(name) {
update_refs.push(name.clone());
assigned_branches.insert(name.clone());
}
}
}
integration_line.push(IntegrationEntry::Pick(CommitEntry {
oid: entry.oid,
short_hash: entry.short_hash.clone(),
message: entry.message.clone(),
command: Command::Pick,
update_refs,
}));
}
}
Ok(Weave {
base_oid: merge_base_oid,
branch_sections,
integration_line,
})
}
pub fn filter_upstream_commits(
&mut self,
repo: &Repository,
workdir: &Path,
new_upstream_oid: Oid,
) -> Result<()> {
let mut candidates: Vec<Oid> = Vec::new();
let mut to_drop = Vec::new();
for section in &self.branch_sections {
for commit in §ion.commits {
match repo.graph_descendant_of(new_upstream_oid, commit.oid) {
Ok(true) => to_drop.push(commit.oid),
Ok(false) => candidates.push(commit.oid),
Err(e) => {
return Err(e.into());
}
}
}
}
if !candidates.is_empty() {
let candidate_set: HashSet<Oid> = candidates.into_iter().collect();
match cherry_pick_equivalents(workdir, &new_upstream_oid, &self.base_oid) {
Some(equivalent) => {
to_drop.extend(equivalent.intersection(&candidate_set).copied());
}
None => {
msg::warn(
"Could not run git cherry — \
cherry-picked commits may not be detected",
);
}
}
}
for oid in to_drop {
self.drop_commit(oid);
}
Ok(())
}
pub fn drop_commit(&mut self, oid: Oid) {
for i in 0..self.branch_sections.len() {
if let Some(pos) = self.branch_sections[i]
.commits
.iter()
.position(|c| c.oid == oid)
{
let removed = self.branch_sections[i].commits.remove(pos);
if !removed.update_refs.is_empty() && !self.branch_sections[i].commits.is_empty() {
let target_pos = if pos > 0 { pos - 1 } else { 0 };
self.branch_sections[i].commits[target_pos]
.update_refs
.extend(removed.update_refs);
}
if self.branch_sections[i].commits.is_empty() {
let label = self.branch_sections[i].label.clone();
self.branch_sections.remove(i);
self.integration_line.retain(
|e| !matches!(e, IntegrationEntry::Merge { label: l, .. } if *l == label),
);
}
return;
}
}
if let Some(pos) = self
.integration_line
.iter()
.position(|e| matches!(e, IntegrationEntry::Pick(c) if c.oid == oid))
&& let IntegrationEntry::Pick(removed) = self.integration_line.remove(pos)
&& !removed.update_refs.is_empty()
{
let target = (pos..self.integration_line.len())
.chain((0..pos).rev())
.find(|&j| matches!(self.integration_line[j], IntegrationEntry::Pick(_)));
if let Some(j) = target
&& let IntegrationEntry::Pick(ref mut c) = self.integration_line[j]
{
c.update_refs.extend(removed.update_refs);
}
}
}
pub fn drop_branch(&mut self, branch_name: &str) {
let Some(idx) = self.branch_sections.iter().position(|s| {
s.branch_names.contains(&branch_name.to_string()) || s.label == branch_name
}) else {
return;
};
let old_label = self.branch_sections[idx].label.clone();
let inner_branch_boundary = self.branch_sections[idx]
.commits
.iter()
.rposition(|c| !c.update_refs.is_empty());
if let Some(boundary) = inner_branch_boundary {
let inner_ref = self.branch_sections[idx].commits[boundary]
.update_refs
.first()
.cloned()
.unwrap();
self.branch_sections[idx].commits.truncate(boundary + 1);
self.branch_sections[idx].label = inner_ref.clone();
self.branch_sections[idx].branch_names = vec![inner_ref.clone()];
let inner = inner_ref.clone();
self.branch_sections[idx].commits[boundary]
.update_refs
.retain(|r| *r != inner);
for entry in &mut self.integration_line {
if let IntegrationEntry::Merge {
label,
original_oid,
} = entry
&& *label == old_label
{
*label = inner_ref.clone();
*original_oid = None;
}
}
} else {
self.branch_sections.remove(idx);
self.integration_line.retain(
|e| !matches!(e, IntegrationEntry::Merge { label: l, .. } if *l == old_label),
);
}
}
pub fn move_commit(&mut self, oid: Oid, to_branch: &str) -> anyhow::Result<()> {
let section_idx = self
.branch_sections
.iter()
.position(|s| s.label == to_branch || s.branch_names.contains(&to_branch.to_string()));
let Some(section_idx) = section_idx else {
anyhow::bail!(
"Cannot move commit: target branch section '{}' not found in weave graph",
to_branch
);
};
let commit = self.remove_commit(oid);
let Some(mut commit) = commit else {
anyhow::bail!(
"Cannot move commit: source commit {} not found in weave graph",
oid
);
};
commit.command = Command::Pick;
if self.branch_sections[section_idx].branch_names.len() > 1
&& self.branch_sections[section_idx]
.branch_names
.contains(&to_branch.to_string())
{
let old_label = self.branch_sections[section_idx].label.clone();
self.branch_sections[section_idx]
.branch_names
.retain(|n| n != to_branch);
if old_label == to_branch
&& let Some(first_remaining) =
self.branch_sections[section_idx].branch_names.first()
{
self.branch_sections[section_idx].label = first_remaining.clone();
}
let base_label = self.branch_sections[section_idx].label.clone();
let new_section = BranchSection {
reset_target: base_label,
commits: vec![commit],
label: to_branch.to_string(),
branch_names: vec![to_branch.to_string()],
};
self.branch_sections.insert(section_idx + 1, new_section);
for entry in &mut self.integration_line {
if let IntegrationEntry::Merge {
label,
original_oid,
} = entry
&& *label == old_label
{
*label = to_branch.to_string();
*original_oid = None;
}
}
} else {
self.branch_sections[section_idx].commits.push(commit);
}
Ok(())
}
pub fn fixup_commit(&mut self, source_oid: Oid, target_oid: Oid) -> anyhow::Result<()> {
let target_in_sections = self
.branch_sections
.iter()
.any(|s| s.commits.iter().any(|c| c.oid == target_oid));
let target_in_integration = self
.integration_line
.iter()
.any(|entry| matches!(entry, IntegrationEntry::Pick(c) if c.oid == target_oid));
if !target_in_sections && !target_in_integration {
anyhow::bail!(
"Cannot fixup commit: target commit {} not found in weave graph",
target_oid
);
}
let commit = self.remove_commit(source_oid);
let Some(mut commit) = commit else {
anyhow::bail!(
"Cannot fixup commit: source commit {} not found in weave graph",
source_oid
);
};
commit.command = Command::Fixup;
for section in &mut self.branch_sections {
if let Some(pos) = section.commits.iter().position(|c| c.oid == target_oid) {
section.commits.insert(pos + 1, commit);
return Ok(());
}
}
for (i, entry) in self.integration_line.iter().enumerate() {
if let IntegrationEntry::Pick(c) = entry
&& c.oid == target_oid
{
self.integration_line
.insert(i + 1, IntegrationEntry::Pick(commit));
return Ok(());
}
}
anyhow::bail!(
"Cannot fixup commit: target commit {} disappeared during operation",
target_oid
)
}
pub fn edit_commit(&mut self, oid: Oid) {
self.set_command(oid, Command::Edit);
}
pub fn add_branch_section(
&mut self,
label: String,
branch_names: Vec<String>,
commits: Vec<CommitEntry>,
reset_target: String,
) {
self.branch_sections.push(BranchSection {
reset_target,
commits,
label,
branch_names,
});
}
pub fn add_merge(&mut self, label: String, original_oid: Option<Oid>, position: Option<usize>) {
let entry = IntegrationEntry::Merge {
original_oid,
label,
};
let idx = position.unwrap_or_else(|| {
self.integration_line
.iter()
.position(|e| matches!(e, IntegrationEntry::Pick(_)))
.unwrap_or(self.integration_line.len())
});
self.integration_line.insert(idx, entry);
}
pub fn weave_branch(&mut self, branch_name: &str) {
let branch_idx = self.integration_line.iter().position(|e| {
matches!(e, IntegrationEntry::Pick(c) if c.update_refs.contains(&branch_name.to_string()))
});
let Some(branch_idx) = branch_idx else {
return;
};
let mut section_commits = Vec::new();
let mut indices_to_remove = Vec::new();
let mut insert_pos = 0;
for i in 0..=branch_idx {
if let IntegrationEntry::Pick(commit) = &self.integration_line[i] {
let mut commit = commit.clone();
commit.update_refs.retain(|r| r != branch_name);
section_commits.push(commit);
indices_to_remove.push(i);
} else {
insert_pos += 1;
}
}
for &i in indices_to_remove.iter().rev() {
self.integration_line.remove(i);
}
self.branch_sections.push(BranchSection {
reset_target: "onto".to_string(),
commits: section_commits,
label: branch_name.to_string(),
branch_names: vec![branch_name.to_string()],
});
self.integration_line.insert(
insert_pos,
IntegrationEntry::Merge {
original_oid: None,
label: branch_name.to_string(),
},
);
}
pub fn reassign_branch(&mut self, drop_branch: &str, keep_branch: &str) {
if let Some(section) = self
.branch_sections
.iter_mut()
.find(|s| s.label == drop_branch || s.branch_names.contains(&drop_branch.to_string()))
{
let old_label = section.label.clone();
if section.label == drop_branch {
section.label = keep_branch.to_string();
}
section.branch_names.retain(|n| n != drop_branch);
if !section.branch_names.contains(&keep_branch.to_string()) {
section.branch_names.push(keep_branch.to_string());
}
let new_label = section.label.clone();
for entry in &mut self.integration_line {
if let IntegrationEntry::Merge {
label,
original_oid,
} = entry
&& *label == old_label
{
*label = new_label.clone();
*original_oid = None;
}
}
}
}
pub fn swap_commits(&mut self, oid_a: Oid, oid_b: Oid) -> Result<()> {
if oid_a == oid_b {
bail!("Cannot swap a commit with itself");
}
let mut sec_a: Option<(usize, usize)> = None;
let mut sec_b: Option<(usize, usize)> = None;
for (si, section) in self.branch_sections.iter().enumerate() {
for (pi, commit) in section.commits.iter().enumerate() {
if commit.oid == oid_a {
sec_a = Some((si, pi));
}
if commit.oid == oid_b {
sec_b = Some((si, pi));
}
}
}
let mut int_a: Option<usize> = None;
let mut int_b: Option<usize> = None;
for (i, entry) in self.integration_line.iter().enumerate() {
if let IntegrationEntry::Pick(c) = entry {
if c.oid == oid_a {
int_a = Some(i);
}
if c.oid == oid_b {
int_b = Some(i);
}
}
}
match (sec_a, sec_b, int_a, int_b) {
(Some((si_a, pi_a)), Some((si_b, pi_b)), _, _) if si_a == si_b => {
self.branch_sections[si_a].commits.swap(pi_a, pi_b);
Ok(())
}
(Some(_), Some(_), _, _) => {
bail!("Cannot swap commits from different branch sections")
}
(None, None, Some(i), Some(j)) => {
self.integration_line.swap(i, j);
Ok(())
}
_ => {
if sec_a.is_none() && int_a.is_none() {
bail!("Commit {} not found in weave graph", oid_a)
} else if sec_b.is_none() && int_b.is_none() {
bail!("Commit {} not found in weave graph", oid_b)
} else {
bail!(
"Cannot swap commits from different locations (branch section vs integration line)"
)
}
}
}
}
fn remove_commit(&mut self, oid: Oid) -> Option<CommitEntry> {
for section in &mut self.branch_sections {
if let Some(pos) = section.commits.iter().position(|c| c.oid == oid) {
return Some(section.commits.remove(pos));
}
}
let idx = self
.integration_line
.iter()
.position(|e| matches!(e, IntegrationEntry::Pick(c) if c.oid == oid));
if let Some(i) = idx
&& let IntegrationEntry::Pick(commit) = self.integration_line.remove(i)
{
return Some(commit);
}
None
}
pub fn track_commit(&mut self, oid: Oid, ref_name: &str) {
for section in &mut self.branch_sections {
for commit in &mut section.commits {
if commit.oid == oid {
commit.update_refs.push(ref_name.to_string());
return;
}
}
}
for entry in &mut self.integration_line {
if let IntegrationEntry::Pick(commit) = entry
&& commit.oid == oid
{
commit.update_refs.push(ref_name.to_string());
return;
}
}
}
fn set_command(&mut self, oid: Oid, command: Command) {
for section in &mut self.branch_sections {
for commit in &mut section.commits {
if commit.oid == oid {
commit.command = command;
return;
}
}
}
for entry in &mut self.integration_line {
if let IntegrationEntry::Pick(commit) = entry
&& commit.oid == oid
{
commit.command = command;
return;
}
}
}
}
fn emit_commits_with_refs(out: &mut String, commits: &[CommitEntry]) -> Vec<String> {
let mut pending_refs: Vec<String> = Vec::new();
for commit in commits {
if commit.command != Command::Fixup && !pending_refs.is_empty() {
flush_refs(out, &pending_refs);
pending_refs.clear();
}
out.push_str(&format!(
"{} {} # {}\n",
commit.command.as_str(),
commit.short_hash,
commit.message
));
pending_refs.extend(commit.update_refs.iter().cloned());
}
pending_refs
}
fn flush_refs(out: &mut String, refs: &[String]) {
for r in refs {
out.push_str(&format!("update-ref refs/heads/{}\n", r));
}
}
#[derive(Debug)]
struct FirstParentEntry {
oid: Oid,
short_hash: String,
message: String,
is_merge: bool,
merge_parent: Option<Oid>,
}
fn walk_first_parent_line(
repo: &Repository,
head: Oid,
stop: Oid,
) -> Result<Vec<FirstParentEntry>> {
let mut entries = Vec::new();
let mut current = head;
let mut visited: HashSet<Oid> = HashSet::new();
while current != stop {
if !visited.insert(current) {
bail!("cycle detected in commit graph at {}", current);
}
let commit = repo.find_commit(current)?;
let short_hash = commit
.as_object()
.short_id()?
.as_str()
.context("short_id is not valid UTF-8")?
.to_string();
let message = repo::commit_subject(&commit);
let is_merge = commit.parent_count() > 1;
if is_merge {
let p0 = commit.parent_id(0)?;
let p1 = commit.parent_id(1)?;
let (continue_parent, branch_parent) =
if p0 == stop || repo.graph_descendant_of(p0, stop).unwrap_or(false) {
(p0, p1)
} else if p1 == stop || repo.graph_descendant_of(p1, stop).unwrap_or(false) {
(p1, p0)
} else {
bail!(
"Neither parent of merge {} leads to merge-base {}",
current,
stop
);
};
entries.push(FirstParentEntry {
oid: current,
short_hash,
message,
is_merge,
merge_parent: Some(branch_parent),
});
current = continue_parent;
} else {
entries.push(FirstParentEntry {
oid: current,
short_hash,
message,
is_merge,
merge_parent: None,
});
current = match commit.parent_id(0) {
Ok(oid) => oid,
Err(_) => {
bail!(
"First-parent walk from {} did not reach merge-base {}",
head,
stop
);
}
};
}
}
entries.reverse();
Ok(entries)
}
fn walk_branch_commits(repo: &Repository, tip: Oid, stop: Oid) -> Result<Vec<BranchCommitEntry>> {
let actual_stop = if tip == stop {
stop
} else {
repo.merge_base(tip, stop).unwrap_or(stop)
};
let mut entries = Vec::new();
let mut current = tip;
let mut visited: HashSet<Oid> = HashSet::new();
while current != actual_stop {
if !visited.insert(current) {
bail!("cycle detected in commit graph at {}", current);
}
let commit = repo.find_commit(current)?;
if commit.parent_count() <= 1 {
let short_hash = commit
.as_object()
.short_id()?
.as_str()
.context("short_id is not valid UTF-8")?
.to_string();
let message = repo::commit_subject(&commit);
entries.push(BranchCommitEntry {
oid: current,
short_hash,
message,
});
}
current = match commit.parent_id(0) {
Ok(oid) => oid,
Err(_) => break,
};
}
Ok(entries)
}
#[derive(Debug)]
struct BranchCommitEntry {
oid: Oid,
short_hash: String,
message: String,
}
pub fn start_edit_rebase(repo: &Repository, workdir: &Path, commit_oid: Oid) -> Result<()> {
if let Ok(mut graph) = Weave::from_repo(repo) {
graph.edit_commit(commit_oid);
let todo = graph.to_todo();
return run_rebase_or_abort(workdir, Some(&graph.base_oid.to_string()), &todo);
}
build_and_run_linear_edit(repo, workdir, commit_oid)
}
fn build_and_run_linear_edit(repo: &Repository, workdir: &Path, commit_oid: Oid) -> Result<()> {
let head_oid = repo::head_oid(repo)?;
let commit = repo.find_commit(commit_oid)?;
let upstream: Option<String> = if commit.parent_count() > 0 {
Some(commit.parent_id(0)?.to_string())
} else {
None
};
let stop = upstream.as_ref().and_then(|s| Oid::from_str(s).ok());
let mut entries = Vec::new();
let mut current = head_oid;
let mut visited: HashSet<Oid> = HashSet::new();
loop {
if Some(current) == stop {
break;
}
if !visited.insert(current) {
bail!("cycle detected in commit graph at {}", current);
}
let c = repo.find_commit(current)?;
let short = c
.as_object()
.short_id()?
.as_str()
.context("Short ID is not valid UTF-8")?
.to_string();
let msg = repo::commit_subject(&c);
let cmd = if current == commit_oid {
"edit"
} else {
"pick"
};
entries.push(format!("{} {} # {}", cmd, short, msg));
if c.parent_count() == 0 {
break;
}
current = c.parent_id(0)?;
}
entries.reverse();
let mut todo = String::from("label onto\n\nreset onto\n");
for line in &entries {
todo.push_str(line);
todo.push('\n');
}
run_rebase_or_abort(workdir, upstream.as_deref(), &todo)
}
pub use crate::git::RebaseOutcome;
pub fn run_rebase_or_abort(
workdir: &Path,
upstream: Option<&str>,
todo_content: &str,
) -> Result<()> {
match run_rebase(workdir, upstream, todo_content)? {
RebaseOutcome::Completed => Ok(()),
RebaseOutcome::Conflicted => {
let _ = git::rebase_abort(workdir);
bail!("Rebase failed with conflicts — aborted");
}
}
}
pub fn run_rebase(
workdir: &Path,
upstream: Option<&str>,
todo_content: &str,
) -> Result<RebaseOutcome> {
use std::io::Write;
use std::process::Command;
use std::time::Instant;
use crate::trace as loom_trace;
let self_exe = git::loom_exe_path()?;
let mut temp_file = tempfile::NamedTempFile::new()?;
temp_file.write_all(todo_content.as_bytes())?;
temp_file.flush()?;
let temp_path = temp_file.into_temp_path();
let exe_str = self_exe.display().to_string().replace('\\', "/");
let source_path = temp_path.display().to_string().replace('\\', "/");
let sequence_editor = format!(
"{} internal-write-todo --source {} ",
shell_escape::unix::escape(exe_str.into()),
shell_escape::unix::escape(source_path.into()),
);
let upstream_arg = upstream.unwrap_or("--root");
let log_args = format!(
"rebase --interactive --autostash --keep-empty --empty=drop --no-autosquash --rebase-merges --update-refs {}",
upstream_arg
);
let mut cmd = Command::new("git");
cmd.current_dir(workdir)
.args([
"rebase",
"--interactive",
"--autostash",
"--keep-empty",
"--empty=drop",
"--no-autosquash",
"--rebase-merges",
"--update-refs",
])
.env("GIT_SEQUENCE_EDITOR", sequence_editor)
.env("GIT_EDITOR", "true");
match upstream {
Some(oid) => {
cmd.arg(oid);
}
None => {
cmd.arg("--root");
}
}
let start = Instant::now();
let output = cmd.output()?;
let duration_ms = start.elapsed().as_millis();
let stderr = String::from_utf8_lossy(&output.stderr);
loom_trace::log_command(
"git",
&log_args,
duration_ms,
output.status.success(),
&stderr,
);
let sidecar = temp_path.with_extension("original");
if let Ok(original_todo) = std::fs::read_to_string(&sidecar) {
let filtered: String = original_todo
.lines()
.filter(|line| !line.starts_with('#'))
.collect::<Vec<_>>()
.join("\n");
loom_trace::annotate("original git todo", &filtered);
let _ = std::fs::remove_file(&sidecar);
}
loom_trace::annotate("generated todo", todo_content);
if !output.status.success() {
let _ = temp_path.close();
if let Ok(git_dir_str) = git::run_git_stdout(workdir, &["rev-parse", "--absolute-git-dir"])
{
let git_dir = std::path::Path::new(git_dir_str.trim());
if git::rebase_is_in_progress(git_dir) {
return Ok(RebaseOutcome::Conflicted);
}
}
let stderr_msg = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git rebase failed: {}", stderr_msg.trim());
}
let _ = temp_path.close();
Ok(RebaseOutcome::Completed)
}
fn cherry_pick_equivalents(workdir: &Path, upstream: &Oid, base: &Oid) -> Option<HashSet<Oid>> {
let stdout = git::run_git_stdout(
workdir,
&["cherry", &upstream.to_string(), "HEAD", &base.to_string()],
)
.ok()?;
Some(
stdout
.lines()
.filter_map(|line| {
line.strip_prefix("- ")
.and_then(|sha| Oid::from_str(sha.trim()).ok())
})
.collect(),
)
}
#[cfg(test)]
#[path = "weave_test.rs"]
mod tests;