use crate::git::run_git_command;
use colored::Colorize;
use std::ops::Not;
use std::process::exit;
pub fn prepare_review_branch(
to_branch: &str,
from_branch: &str,
skip_to: Option<&str>,
stop_at: Option<&str>,
verbose: bool,
) {
let review_branch = format!("review-{}-{}", to_branch, from_branch);
run_git_command(
&format!("switch to {} branch", from_branch),
&["switch", from_branch],
false,
verbose,
);
run_git_command(
&format!("pull {} branch", from_branch),
&["pull", "origin", from_branch],
false,
verbose,
);
run_git_command(
&format!("switch to {} branch", to_branch),
&["switch", to_branch],
false,
verbose,
);
run_git_command(
&format!("pull {} branch", to_branch),
&["pull", "origin", to_branch],
false,
verbose,
);
let merge_base_output = run_git_command(
"get merge base",
&["merge-base", to_branch, from_branch],
false,
verbose,
);
let merge_base = String::from_utf8_lossy(&merge_base_output.stdout)
.trim()
.to_string();
let valid_commits = run_git_command(
"get valid commit range",
&["rev-list", &format!("{}..{}", merge_base, from_branch)],
false,
verbose,
);
let valid_list = String::from_utf8_lossy(&valid_commits.stdout);
let valid_hashes: Vec<&str> = valid_list.lines().collect();
if let Some(hash) = skip_to {
let is_valid = valid_hashes.iter().any(|line| line.starts_with(hash));
if !is_valid {
eprintln!(
"{}: Commit {} is not in the range {}..{}",
"error".red().bold(),
hash,
to_branch,
from_branch
);
exit(1);
}
}
if let Some(hash) = stop_at {
let is_valid = valid_hashes.iter().any(|line| line.starts_with(hash));
if !is_valid {
eprintln!(
"{}: Commit {} is not in the range {}..{}",
"error".red().bold(),
hash,
to_branch,
from_branch
);
exit(1);
}
if let Some(skip_hash) = skip_to {
let skip_to_commits = run_git_command(
"get commits after skip_to",
&["rev-list", &format!("{}..{}", skip_hash, from_branch)],
false,
verbose,
);
let skip_to_list = String::from_utf8_lossy(&skip_to_commits.stdout);
let is_after_skip = skip_to_list.lines().any(|line| line.starts_with(hash))
|| valid_hashes
.iter()
.any(|line| line.starts_with(hash) && line.starts_with(skip_hash));
let stop_at_equals_skip_to = valid_hashes
.iter()
.any(|line| line.starts_with(hash) && line.starts_with(skip_hash));
if !is_after_skip && !stop_at_equals_skip_to {
eprintln!(
"{}: --stop-at ({}) must be at or after --skip-to ({})",
"error".red().bold(),
hash,
skip_hash
);
exit(1);
}
}
}
let review_branch_exists = run_git_command(
"check existence of review branch",
&[
"show-ref",
"--verify",
&format!("refs/heads/{}", review_branch),
],
true,
verbose,
)
.status
.success();
if review_branch_exists {
run_git_command(
"switch to review branch",
&["switch", &review_branch],
false,
verbose,
);
} else {
run_git_command(
"create review branch from merge-base",
&["checkout", "-b", &review_branch, &merge_base],
false,
verbose,
);
}
let target_commit = if let Some(hash) = skip_to {
let parent = format!("{}^", hash);
let has_earlier = run_git_command(
"check earlier commits",
&["rev-list", &format!("{}..{}", merge_base, &parent)],
true,
verbose,
);
if !has_earlier.stdout.is_empty() {
run_git_command(
"auto-approve earlier commits",
&[
"merge",
"--squash",
"--quiet",
"--no-stat",
"-X",
"theirs",
&parent,
],
false,
verbose,
);
run_git_command(
"commit auto-approved changes",
&["commit", "--quiet", "-m", "Auto-approve earlier commits"],
false,
verbose,
);
}
stop_at.unwrap_or(from_branch).to_string()
} else {
stop_at.unwrap_or(from_branch).to_string()
};
run_git_command(
"squash merge remaining changes",
&[
"merge",
"--squash",
"--quiet",
"--no-stat",
"-X",
"theirs",
&target_commit,
],
false,
verbose,
);
run_git_command("unstage changes for review", &["reset"], false, verbose);
}
pub fn approve_changes(verbose: bool) -> Result<(), ()> {
let has_staged_changes = run_git_command(
"check staged changes",
&["diff", "--cached"],
false,
verbose,
)
.stdout
.is_empty()
.not();
if has_staged_changes {
run_git_command(
"commit reviewed changes",
&["commit", "--quiet", "-m", "Approve reviewed changes"],
false,
verbose,
);
}
run_git_command(
"discard unreviewed changes",
&["restore", "--source=HEAD", "--worktree", "--", "."],
false,
verbose,
);
run_git_command("discard untracked files", &["clean", "-fd"], false, verbose);
match has_staged_changes {
true => Ok(()),
false => Err(()),
}
}
pub struct ReviewStatus {
pub from_branch: String,
pub file_count: usize,
pub insertions: usize,
pub deletions: usize,
pub files: Vec<String>,
}
pub fn get_review_status(from_branch: &str, verbose: bool) -> ReviewStatus {
let stat_output = run_git_command(
"get diff stats",
&["diff", "--stat", "HEAD", from_branch],
false,
verbose,
);
let stat_str = String::from_utf8_lossy(&stat_output.stdout);
let mut file_count = 0;
let mut insertions = 0;
let mut deletions = 0;
if let Some(last_line) = stat_str.lines().last() {
for part in last_line.split(',') {
let part = part.trim();
if part.contains("file") {
if let Some(num) = part.split_whitespace().next() {
file_count = num.parse().unwrap_or(0);
}
} else if part.contains("insertion") {
if let Some(num) = part.split_whitespace().next() {
insertions = num.parse().unwrap_or(0);
}
} else if part.contains("deletion") {
if let Some(num) = part.split_whitespace().next() {
deletions = num.parse().unwrap_or(0);
}
}
}
}
let files_output = run_git_command(
"get changed files",
&["diff", "--name-only", "HEAD", from_branch],
false,
verbose,
);
let files: Vec<String> = String::from_utf8_lossy(&files_output.stdout)
.lines()
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
ReviewStatus {
from_branch: from_branch.to_string(),
file_count,
insertions,
deletions,
files,
}
}