use std::path::Path;
use std::process::Command;
use super::helpers::{
ensure_git_repo, git_remote_url, parse_args, require_confirm, require_string_field,
run_and_format,
};
pub fn checkout(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
let target = match require_string_field(&v, "target") {
Ok(s) => s,
Err(e) => return e,
};
if target.contains("..") {
return "错误:target 不能包含 ..".to_string();
}
let create = v.get("create").and_then(|x| x.as_bool()).unwrap_or(false);
let mut cmd = Command::new("git");
if create {
cmd.arg("checkout").arg("-b").arg(target);
} else {
cmd.arg("checkout").arg(target);
}
cmd.current_dir(working_dir);
run_and_format(cmd, max_output_len, "git checkout")
}
pub fn branch_create(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
let name = match require_string_field(&v, "name") {
Ok(s) => s,
Err(e) => return e,
};
if name.contains("..") || name.starts_with('-') {
return "错误:分支名不合法".to_string();
}
let start_point = v
.get("start_point")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let mut cmd = Command::new("git");
cmd.arg("branch").arg(name);
if let Some(sp) = start_point {
cmd.arg(sp);
}
cmd.current_dir(working_dir);
run_and_format(cmd, max_output_len, "git branch(创建)")
}
pub fn branch_delete(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
if let Err(e) = require_confirm(&v, "git_branch_delete") {
return e;
}
let name = match require_string_field(&v, "name") {
Ok(s) => s,
Err(e) => return e,
};
let force = v.get("force").and_then(|x| x.as_bool()).unwrap_or(false);
let mut cmd = Command::new("git");
cmd.arg("branch");
if force {
cmd.arg("-D");
} else {
cmd.arg("-d");
}
cmd.arg(name).current_dir(working_dir);
run_and_format(cmd, max_output_len, "git branch(删除)")
}
pub fn push(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
if let Err(e) = require_confirm(&v, "git_push") {
return e;
}
let remote = v
.get("remote")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or("origin");
let branch = v
.get("branch")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let set_upstream = v
.get("set_upstream")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let force_with_lease = v
.get("force_with_lease")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let tags = v.get("tags").and_then(|x| x.as_bool()).unwrap_or(false);
let mut cmd = Command::new("git");
cmd.arg("push");
if set_upstream {
cmd.arg("-u");
}
if force_with_lease {
cmd.arg("--force-with-lease");
}
if tags {
cmd.arg("--tags");
}
cmd.arg(remote);
if let Some(b) = branch {
cmd.arg(b);
}
cmd.current_dir(working_dir);
if let Some(url) = git_remote_url(working_dir, remote) {
crate::cm_tools::github_token::apply_github_https_auth(&mut cmd, &url);
}
run_and_format(cmd, max_output_len, "git push")
}
pub fn merge(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
if let Err(e) = require_confirm(&v, "git_merge") {
return e;
}
let branch = match require_string_field(&v, "branch") {
Ok(s) => s,
Err(e) => return e,
};
let no_ff = v.get("no_ff").and_then(|x| x.as_bool()).unwrap_or(false);
let squash = v.get("squash").and_then(|x| x.as_bool()).unwrap_or(false);
let message = v
.get("message")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let mut cmd = Command::new("git");
cmd.arg("merge");
if no_ff {
cmd.arg("--no-ff");
}
if squash {
cmd.arg("--squash");
}
if let Some(m) = message {
cmd.arg("-m").arg(m);
}
cmd.arg(branch).current_dir(working_dir);
run_and_format(cmd, max_output_len, "git merge")
}
pub fn rebase(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
if let Err(e) = require_confirm(&v, "git_rebase") {
return e;
}
let onto = v
.get("onto")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let abort = v.get("abort").and_then(|x| x.as_bool()).unwrap_or(false);
let cont = v.get("continue").and_then(|x| x.as_bool()).unwrap_or(false);
let mut cmd = Command::new("git");
cmd.arg("rebase");
if abort {
cmd.arg("--abort");
} else if cont {
cmd.arg("--continue");
} else if let Some(target) = onto {
cmd.arg(target);
} else {
return "错误:rebase 需要 onto 参数,或 abort=true / continue=true".to_string();
}
cmd.current_dir(working_dir);
run_and_format(cmd, max_output_len, "git rebase")
}
pub fn stash(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
let action = v
.get("action")
.and_then(|x| x.as_str())
.map(str::trim)
.unwrap_or("push");
let message = v
.get("message")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
let mut cmd = Command::new("git");
match action {
"push" | "save" => {
cmd.arg("stash").arg("push");
if let Some(m) = message {
cmd.arg("-m").arg(m);
}
}
"pop" => {
cmd.arg("stash").arg("pop");
}
"apply" => {
cmd.arg("stash").arg("apply");
}
"list" => {
cmd.arg("stash").arg("list");
}
"drop" => {
cmd.arg("stash").arg("drop");
}
"clear" => {
if let Err(e) = require_confirm(&v, "git_stash clear") {
return e;
}
cmd.arg("stash").arg("clear");
}
_ => return format!("错误:不支持的 stash action: {}", action),
}
cmd.current_dir(working_dir);
run_and_format(cmd, max_output_len, &format!("git stash {}", action))
}
pub fn tag(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
let action = v
.get("action")
.and_then(|x| x.as_str())
.map(str::trim)
.unwrap_or("list");
if action == "delete"
&& let Err(e) = require_confirm(&v, "git_tag delete")
{
return e;
}
let args = match build_git_tag_args(&v, action) {
Ok(a) => a,
Err(e) => return e,
};
let mut cmd = Command::new("git");
cmd.args(&args).current_dir(working_dir);
run_and_format(cmd, max_output_len, &format!("git tag {}", action))
}
fn build_git_tag_args(v: &serde_json::Value, action: &str) -> Result<Vec<String>, String> {
match action {
"list" => {
let mut args = vec!["tag".to_string(), "-l".to_string()];
if let Some(p) = v
.get("pattern")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
{
args.push(p.to_string());
}
Ok(args)
}
"create" => {
let name = match v.get("name").and_then(|x| x.as_str()).map(str::trim) {
Some(s) if !s.is_empty() => s,
_ => return Err("错误:创建 tag 需要 name 参数".to_string()),
};
if name.contains("..") || name.starts_with('-') {
return Err("错误:tag 名不合法".to_string());
}
let message = v
.get("message")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
Ok(match message {
Some(m) => vec![
"tag".into(),
"-a".into(),
name.into(),
"-m".into(),
m.into(),
],
None => vec!["tag".into(), name.into()],
})
}
"delete" => {
let name = match v.get("name").and_then(|x| x.as_str()).map(str::trim) {
Some(s) if !s.is_empty() => s,
_ => return Err("错误:删除 tag 需要 name 参数".to_string()),
};
Ok(vec!["tag".into(), "-d".into(), name.into()])
}
_ => Err(format!("错误:不支持的 tag action: {}", action)),
}
}
pub fn reset(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
if let Err(e) = require_confirm(&v, "git_reset") {
return e;
}
let mode = v
.get("mode")
.and_then(|x| x.as_str())
.map(str::trim)
.unwrap_or("mixed");
let target = v
.get("target")
.and_then(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or("HEAD");
let flag = match mode {
"soft" => "--soft",
"mixed" => "--mixed",
"hard" => "--hard",
_ => return format!("错误:不支持的 reset mode: {}(仅 soft/mixed/hard)", mode),
};
let mut cmd = Command::new("git");
cmd.arg("reset").arg(flag).arg(target);
cmd.current_dir(working_dir);
run_and_format(
cmd,
max_output_len,
&format!("git reset {} {}", flag, target),
)
}
fn cherry_pick_commit_list(v: &serde_json::Value) -> Result<Vec<String>, String> {
match v.get("commits").and_then(|x| x.as_array()) {
Some(arr) if !arr.is_empty() => Ok(arr
.iter()
.filter_map(|x| x.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.map(ToString::to_string)
.collect()),
_ => match v.get("commit").and_then(|x| x.as_str()).map(str::trim) {
Some(s) if !s.is_empty() => Ok(vec![s.to_string()]),
_ => Err("错误:缺少 commit(s) 参数".to_string()),
},
}
}
fn apply_cherry_pick_subcommand(cmd: &mut Command, v: &serde_json::Value) -> Result<(), String> {
let abort = v.get("abort").and_then(|x| x.as_bool()).unwrap_or(false);
let cont = v.get("continue").and_then(|x| x.as_bool()).unwrap_or(false);
if abort {
cmd.arg("--abort");
return Ok(());
}
if cont {
cmd.arg("--continue");
return Ok(());
}
let commits = cherry_pick_commit_list(v)?;
let no_commit = v
.get("no_commit")
.and_then(|x| x.as_bool())
.unwrap_or(false);
if no_commit {
cmd.arg("--no-commit");
}
for c in &commits {
cmd.arg(c);
}
Ok(())
}
pub fn cherry_pick(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
if let Err(e) = require_confirm(&v, "git_cherry_pick") {
return e;
}
let mut cmd = Command::new("git");
cmd.arg("cherry-pick");
if let Err(e) = apply_cherry_pick_subcommand(&mut cmd, &v) {
return e;
}
cmd.current_dir(working_dir);
run_and_format(cmd, max_output_len, "git cherry-pick")
}
pub fn revert(args_json: &str, max_output_len: usize, working_dir: &Path) -> String {
let v = match parse_args(args_json) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = ensure_git_repo(working_dir) {
return e;
}
if let Err(e) = require_confirm(&v, "git_revert") {
return e;
}
let abort = v.get("abort").and_then(|x| x.as_bool()).unwrap_or(false);
let cont = v.get("continue").and_then(|x| x.as_bool()).unwrap_or(false);
let mut cmd = Command::new("git");
cmd.arg("revert");
if abort {
cmd.arg("--abort");
} else if cont {
cmd.arg("--continue");
} else {
let commit = match require_string_field(&v, "commit") {
Ok(s) => s,
Err(e) => return e,
};
let no_commit = v
.get("no_commit")
.and_then(|x| x.as_bool())
.unwrap_or(false);
if no_commit {
cmd.arg("--no-commit");
}
cmd.arg(commit);
}
cmd.current_dir(working_dir);
run_and_format(cmd, max_output_len, "git revert")
}
#[cfg(test)]
mod tests {
use super::super::helpers::is_safe_rel_path;
#[test]
fn test_is_safe_rel_path() {
assert!(is_safe_rel_path("src/main.rs"));
assert!(!is_safe_rel_path("/etc/passwd"));
assert!(!is_safe_rel_path("../x"));
assert!(!is_safe_rel_path(""));
}
}