`quicommit ginit`(别名 `gi`)离线完成仓库起步:用 git2 建库并确立初始分支、 写最简 README.md、按根目录标志文件装配 .gitignore、按 --add 添加远程, `--profile` 可选套用已有 Git 配置。在已有仓库上重复执行只补齐缺失项:绝不覆盖 既有的 README.md/.gitignore、绝不改动已有 remote 的地址、不提供 --force、 不创建首个提交。边界取舍见 ADR 0006,命令命名见 ADR 0005。 实现落在新的 src/git/init.rs(9 个 git 层函数)与 src/commands/ginit.rs, 命令层不直接引用 git2,GitRepo 公开接口未改动,其余命令行为不变。 测试:src/git/init.rs 11 条单元测试、tests/ginit_tests.rs 18 条集成测试; 新增 35 个 ginit_* 消息覆盖 7 种语言。 另将 CONTEXT.md 与 docs/adr/ 纳入版本跟踪(.gitignore 放开 adr/ 与 CONTEXT.md 两条忽略规则);RAODMAP.md 除 ginit 条目外还包含此前未提交的 规划条目更新。
107 lines
3.2 KiB
Rust
107 lines
3.2 KiB
Rust
pub mod changelog;
|
|
pub mod commit;
|
|
pub mod config;
|
|
pub mod credential;
|
|
pub mod ginit;
|
|
pub mod init;
|
|
pub mod profile;
|
|
pub mod tag;
|
|
|
|
use anyhow::{Result, bail};
|
|
use dialoguer::{Confirm, MultiSelect};
|
|
|
|
use crate::git::GitRepo;
|
|
use crate::i18n::Messages;
|
|
use crate::utils::{print_progress, print_success, print_warning};
|
|
|
|
/// Shared push flow for commit and tag: resolve the push target, honor the
|
|
/// `--yes` > `--select-remote` > `--push` priority chain, then push to the
|
|
/// chosen target(s). `progress_msg`/`success_msg` render the per-remote
|
|
/// status lines; `refspec` is "HEAD" for commit, the tag ref for tag.
|
|
pub(crate) fn run_push_flow(
|
|
repo: &GitRepo,
|
|
requested_remote: &str,
|
|
select_remote: bool,
|
|
push: bool,
|
|
yes: bool,
|
|
messages: &Messages,
|
|
confirm_prompt: &str,
|
|
refspec: &str,
|
|
progress_msg: impl Fn(&str) -> String,
|
|
success_msg: impl Fn(&str) -> String,
|
|
) -> Result<()> {
|
|
let Some((target, fell_back)) = repo.resolve_push_target(requested_remote)? else {
|
|
print_warning(&messages.push_skipped_no_remote());
|
|
return Ok(());
|
|
};
|
|
if fell_back && (push || !yes) {
|
|
print_warning(&messages.push_target_fallback(requested_remote, &target));
|
|
}
|
|
|
|
let remotes = if select_remote && !yes {
|
|
Some(repo.list_remotes()?)
|
|
} else {
|
|
None
|
|
};
|
|
let use_multi_select = remotes.as_ref().is_some_and(|r| r.len() > 1);
|
|
|
|
let targets: Vec<String> = if use_multi_select {
|
|
let remotes = remotes.unwrap();
|
|
let items: Vec<(&String, bool)> = remotes.iter().map(|r| (r, *r == target)).collect();
|
|
let selected = MultiSelect::new()
|
|
.with_prompt(messages.select_push_remotes())
|
|
.items_checked(&items)
|
|
.interact()?;
|
|
if selected.is_empty() {
|
|
print_warning(&messages.no_remotes_selected());
|
|
return Ok(());
|
|
}
|
|
selected.iter().map(|&i| remotes[i].clone()).collect()
|
|
} else if push {
|
|
vec![target]
|
|
} else if !yes {
|
|
if Confirm::new()
|
|
.with_prompt(confirm_prompt)
|
|
.default(false)
|
|
.interact()?
|
|
{
|
|
vec![target]
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
if targets.len() == 1 {
|
|
print_progress(&progress_msg(&targets[0]));
|
|
repo.push(&targets[0], refspec)?;
|
|
print_success(&success_msg(&targets[0]));
|
|
} else if targets.len() > 1 {
|
|
let fmt_list = |items: &[String]| {
|
|
if items.is_empty() {
|
|
"-".to_string()
|
|
} else {
|
|
items.join(", ")
|
|
}
|
|
};
|
|
for (i, remote_name) in targets.iter().enumerate() {
|
|
print_progress(&progress_msg(remote_name));
|
|
if let Err(e) = repo.push(remote_name, refspec) {
|
|
bail!(
|
|
"{}\n{}",
|
|
messages.push_multi_failed(
|
|
remote_name,
|
|
&fmt_list(&targets[..i]),
|
|
&fmt_list(&targets[i + 1..])
|
|
),
|
|
e
|
|
);
|
|
}
|
|
print_success(&success_msg(remote_name));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|