use crate::git::Git;
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BaseSource {
Flag,
Config,
Remote,
Unresolved,
}
#[derive(Debug, Clone)]
pub struct ResolvedBase {
pub name: Option<String>,
pub source: BaseSource,
}
impl ResolvedBase {
fn flag(name: &str) -> Self {
Self {
name: Some(name.to_string()),
source: BaseSource::Flag,
}
}
fn config(name: &str) -> Self {
Self {
name: Some(name.to_string()),
source: BaseSource::Config,
}
}
fn remote(name: &str) -> Self {
Self {
name: Some(name.to_string()),
source: BaseSource::Remote,
}
}
pub fn unresolved() -> Self {
Self {
name: None,
source: BaseSource::Unresolved,
}
}
pub fn describe(&self) -> String {
match (&self.name, self.source) {
(Some(b), BaseSource::Flag) => format!("{b} (from --base-branch)"),
(Some(b), BaseSource::Config) => format!("{b} (from git config gkit.baseBranch)"),
(Some(b), BaseSource::Remote) => format!("{b} (derived from remote origin/{b})"),
_ => "UNRESOLVED — gkit.baseBranch unset and no origin/main or origin/master \
(correct-branch can't be checked)"
.to_string(),
}
}
}
pub fn resolve_base(git: &dyn Git, dir: &Path, cli_override: Option<&str>) -> ResolvedBase {
if let Some(b) = cli_override {
let b = b.trim();
if !b.is_empty() {
return ResolvedBase::flag(b);
}
}
let cfg = git.run(dir, &["config", "--get", "gkit.baseBranch"]);
if cfg.success && !cfg.trimmed().is_empty() {
return ResolvedBase::config(cfg.trimmed());
}
let remotes: HashSet<String> = git
.run(
dir,
&[
"for-each-ref",
"--format=%(refname:short)",
"refs/remotes/origin/*",
],
)
.stdout
.lines()
.filter_map(|l| l.trim().strip_prefix("origin/").map(str::to_string))
.collect();
for cand in ["main", "master"] {
if remotes.contains(cand) {
return ResolvedBase::remote(cand);
}
}
ResolvedBase::unresolved()
}
pub fn resolve_solo(git: &dyn Git, dir: &Path) -> bool {
let o = git.run(dir, &["config", "--get", "--bool", "gkit.solo"]);
o.success && o.trimmed() == "true"
}
pub fn current_branch_opt(git: &dyn Git, dir: &Path) -> Option<String> {
let o = git.run(dir, &["symbolic-ref", "--short", "HEAD"]);
if o.success {
Some(o.trimmed().to_string())
} else {
None
}
}
pub fn resolve_switch_base(
git: &dyn Git,
dir: &Path,
cli_override: Option<&str>,
) -> Option<String> {
if let Some(b) = cli_override {
if !b.trim().is_empty() {
return Some(b.trim().to_string());
}
}
let cfg = git.run(dir, &["config", "--get", "gkit.baseBranch"]);
if cfg.success && !cfg.trimmed().is_empty() {
return Some(cfg.trimmed().to_string());
}
let head = git.run(
dir,
&["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
);
if head.success {
return head.trimmed().strip_prefix("origin/").map(str::to_string);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::test_support::FakeGit;
use std::path::Path;
fn d() -> &'static Path {
Path::new("/x")
}
fn with_remotes(g: FakeGit, branches: &[&str]) -> FakeGit {
let listing = branches
.iter()
.map(|b| format!("origin/{b}"))
.collect::<Vec<_>>()
.join("\n");
g.ok(
"for-each-ref --format=%(refname:short) refs/remotes/origin/*",
&listing,
)
}
#[test]
fn cli_override_wins() {
let g = FakeGit::new().ok("config --get gkit.baseBranch", "dev");
let r = resolve_base(&g, d(), Some("main"));
assert_eq!(r.name.as_deref(), Some("main"));
assert_eq!(r.source, BaseSource::Flag);
}
#[test]
fn falls_back_to_git_config() {
let g = FakeGit::new().ok("config --get gkit.baseBranch", "dev");
let r = resolve_base(&g, d(), None);
assert_eq!(r.name.as_deref(), Some("dev"));
assert_eq!(r.source, BaseSource::Config);
}
#[test]
fn derives_main_from_remote_when_config_unset() {
let g = with_remotes(
FakeGit::new().fail("config --get gkit.baseBranch"),
&["feature-x", "main", "master"],
);
let r = resolve_base(&g, d(), None);
assert_eq!(r.name.as_deref(), Some("main"));
assert_eq!(r.source, BaseSource::Remote);
}
#[test]
fn derives_master_when_no_main() {
let g = with_remotes(
FakeGit::new().fail("config --get gkit.baseBranch"),
&["master", "feature-y"],
);
let r = resolve_base(&g, d(), None);
assert_eq!(r.name.as_deref(), Some("master"));
assert_eq!(r.source, BaseSource::Remote);
}
#[test]
fn unresolved_when_no_config_and_no_main_master() {
let g = with_remotes(
FakeGit::new().fail("config --get gkit.baseBranch"),
&["feature-only"],
);
let r = resolve_base(&g, d(), None);
assert_eq!(r.name, None);
assert_eq!(r.source, BaseSource::Unresolved);
}
#[test]
fn resolve_solo_defaults_false_and_reads_bool() {
assert!(!resolve_solo(
&FakeGit::new().fail("config --get --bool gkit.solo"),
d()
));
assert!(resolve_solo(
&FakeGit::new().ok("config --get --bool gkit.solo", "true"),
d()
));
assert!(!resolve_solo(
&FakeGit::new().ok("config --get --bool gkit.solo", "false"),
d()
));
}
#[test]
fn current_branch_opt_detects_detached() {
let on = FakeGit::new().ok("symbolic-ref --short HEAD", "feat");
assert_eq!(current_branch_opt(&on, d()), Some("feat".into()));
let detached = FakeGit::new().fail("symbolic-ref --short HEAD");
assert_eq!(current_branch_opt(&detached, d()), None);
}
#[test]
fn switch_base_uses_origin_head_not_current() {
let g = FakeGit::new().fail("config --get gkit.baseBranch").ok(
"symbolic-ref --short refs/remotes/origin/HEAD",
"origin/dev",
);
assert_eq!(resolve_switch_base(&g, d(), None), Some("dev".into()));
assert_eq!(
resolve_switch_base(&g, d(), Some("main")),
Some("main".into())
);
}
}