From 1b1621642960bdf9665c1b5588f1da99bfb52f6e Mon Sep 17 00:00:00 2001 From: Gregorios Leach Date: Tue, 28 Apr 2026 22:02:44 -0700 Subject: [PATCH] feat: resolve stack base from forge CLI when config omits repo.base Add stack_base_hint::try_from_forge_cli as the extension point for hosting defaults; implement GitHub via gh repo view defaultBranchRef (origin/ or local branch when present). Precedence remains: explicit [repo].base, forge CLI hint, then git heuristics. Pass forge type from repo_loader into resolve_base. Made-with: Cursor --- src/forge/github.rs | 39 ++++++++++++++++++++++++++++++++++++ src/forge/mod.rs | 38 ++++++++++++++++++++++++++++------- src/forge/stack_base_hint.rs | 18 +++++++++++++++++ src/git/ops.rs | 12 ++++++++--- src/git/repo_loader.rs | 6 +++++- 5 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 src/forge/stack_base_hint.rs diff --git a/src/forge/github.rs b/src/forge/github.rs index 2e6c896..fe590d4 100644 --- a/src/forge/github.rs +++ b/src/forge/github.rs @@ -7,6 +7,45 @@ use super::Forge; use crate::core::stack::{PatchEntry, PatchStatus}; use crate::git::ops::Repo; +/// If `gh` reports the repo default branch, return a ref that exists locally (`origin/` first, +/// then ``). Called from [`crate::forge::stack_base_hint::try_from_forge_cli`] for [`crate::forge::ForgeKind::GitHub`]. +pub(crate) fn try_cli_default_stack_base(repo: &Repo) -> Option { + let output = Command::new("gh") + .current_dir(&repo.workdir) + .args([ + "repo", + "view", + "--json", + "defaultBranchRef", + "--jq", + ".defaultBranchRef.name", + ]) + .stderr(std::process::Stdio::null()) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let name = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if name.is_empty() || name == "null" { + return None; + } + let origin_ref = format!("origin/{}", name); + if repo + .git_pub(&["rev-parse", "--verify", "--quiet", &origin_ref]) + .is_ok() + { + return Some(origin_ref); + } + if repo + .git_pub(&["rev-parse", "--verify", "--quiet", &name]) + .is_ok() + { + return Some(name); + } + None +} + pub struct GitHub; impl Forge for GitHub { diff --git a/src/forge/mod.rs b/src/forge/mod.rs index e704545..1cd85d4 100644 --- a/src/forge/mod.rs +++ b/src/forge/mod.rs @@ -3,9 +3,34 @@ pub mod gitea; pub mod github; pub mod gitlab; pub mod phabricator; +pub mod stack_base_hint; use std::collections::HashMap; +/// Forge platform from `.pilegit.toml` `[forge] type = ...`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ForgeKind { + GitHub, + GitLab, + Gitea, + Phabricator, + Custom, +} + +impl ForgeKind { + /// Parse from config `type` string. Unknown values match [`create_forge`]: treated as GitHub. + pub fn from_config_str(s: &str) -> Self { + match s { + "github" => Self::GitHub, + "gitlab" => Self::GitLab, + "gitea" => Self::Gitea, + "phabricator" => Self::Phabricator, + "custom" => Self::Custom, + _ => Self::GitHub, + } + } +} + use color_eyre::Result; use crate::core::config::Config; @@ -151,14 +176,13 @@ pub trait Forge { /// Create the appropriate Forge based on config. pub fn create_forge(config: &Config) -> Box { - match config.forge.forge_type.as_str() { - "github" => Box::new(github::GitHub), - "gitlab" => Box::new(gitlab::GitLab), - "gitea" => Box::new(gitea::Gitea), - "phabricator" => Box::new(phabricator::Phabricator), - "custom" => Box::new(custom::Custom::new( + match ForgeKind::from_config_str(config.forge.forge_type.as_str()) { + ForgeKind::GitHub => Box::new(github::GitHub), + ForgeKind::GitLab => Box::new(gitlab::GitLab), + ForgeKind::Gitea => Box::new(gitea::Gitea), + ForgeKind::Phabricator => Box::new(phabricator::Phabricator), + ForgeKind::Custom => Box::new(custom::Custom::new( config.forge.submit_cmd.clone().unwrap_or_default(), )), - _ => Box::new(github::GitHub), } } diff --git a/src/forge/stack_base_hint.rs b/src/forge/stack_base_hint.rs new file mode 100644 index 0000000..c511ba3 --- /dev/null +++ b/src/forge/stack_base_hint.rs @@ -0,0 +1,18 @@ +//! When `[repo].base` is unset, optional detection of the default branch via each forge's CLI. +//! +//! Precedence is implemented in [`crate::git::ops::Repo::resolve_base`]: explicit config, then +//! [`try_from_forge_cli`], then git ref heuristics. Add new [`crate::forge::ForgeKind`] arms here +//! when a CLI can report the hosting default (e.g. GitLab `glab`, Gitea `tea`). + +use crate::forge::ForgeKind; +use crate::git::ops::Repo; + +/// Best-effort default stack base from the configured forge's CLI (`None` → fall back to git heuristics). +pub fn try_from_forge_cli(repo: &Repo, forge: ForgeKind) -> Option { + match forge { + ForgeKind::GitHub => super::github::try_cli_default_stack_base(repo), + // Future: ForgeKind::GitLab => super::gitlab::try_cli_default_stack_base(repo), + // Future: ForgeKind::Gitea => super::gitea::try_cli_default_stack_base(repo), + ForgeKind::GitLab | ForgeKind::Gitea | ForgeKind::Phabricator | ForgeKind::Custom => None, + } +} diff --git a/src/git/ops.rs b/src/git/ops.rs index 94aa06d..7805fc2 100644 --- a/src/git/ops.rs +++ b/src/git/ops.rs @@ -4,6 +4,7 @@ use std::process::Command; use color_eyre::{eyre::eyre, Result}; use crate::core::stack::{PatchEntry, PatchStatus}; +use crate::forge::ForgeKind; /// Wrapper around a git repository. pub struct Repo { @@ -39,8 +40,9 @@ impl Repo { } } - /// Use explicit `repo.base` from config when valid, otherwise [`Self::detect_base`]. - pub fn resolve_base(&self, configured: Option<&str>) -> Result { + /// Resolve stack base: explicit `[repo].base`, then [`crate::forge::stack_base_hint`] for the + /// configured forge (CLI default branch when implemented), then [`Self::detect_base`]. + pub fn resolve_base(&self, configured: Option<&str>, forge: ForgeKind) -> Result { if let Some(b) = configured { let b = b.trim(); if !b.is_empty() { @@ -54,6 +56,9 @@ impl Repo { )); } } + if let Some(b) = crate::forge::stack_base_hint::try_from_forge_cli(self, forge) { + return Ok(b); + } self.detect_base() } @@ -76,7 +81,8 @@ impl Repo { } } Err(eyre!( - "Could not detect base branch (tried origin/main, origin/master, main, master). \ + "Could not detect base branch (tried forge CLI default for your `[forge].type`, \ + then origin/main, origin/master, main, master). \ Set repo.base in .pilegit.toml (for example base = \"origin/develop\") or run `pgit init`." )) } diff --git a/src/git/repo_loader.rs b/src/git/repo_loader.rs index 9410e25..b91c6c9 100644 --- a/src/git/repo_loader.rs +++ b/src/git/repo_loader.rs @@ -4,6 +4,7 @@ use color_eyre::Result; use super::ops::Repo; use crate::core::config::{Config, ForgeConfig, RepoConfig}; +use crate::forge::ForgeKind; /// Open the current git repo and resolve the stack base from config (if any) or heuristics. pub fn open_resolved() -> Result { @@ -15,6 +16,9 @@ pub fn open_resolved() -> Result { }, repo: RepoConfig::default(), }); - let base = repo.resolve_base(config.repo.base.as_deref())?; + let base = repo.resolve_base( + config.repo.base.as_deref(), + ForgeKind::from_config_str(config.forge.forge_type.as_str()), + )?; Ok(repo.with_resolved_base(base)) }