use std::time::Duration;
use anyhow::{Context, Result, bail};
use crate::cli::{FetchMode, PushMode};
use crate::git;
pub const PROVIDER_KEY: &str = "stk.provider";
pub const REMOTE_KEY: &str = "stk.remote";
pub const UPDATE_REFS_KEY: &str = "stk.updateRefs";
pub const FETCH_BEFORE_RESTACK_KEY: &str = "stk.fetchBeforeRestack";
pub const PUSH_ON_RESTACK_KEY: &str = "stk.pushOnRestack";
pub const PUSH_ON_SUBMIT_KEY: &str = "stk.pushOnSubmit";
pub const SUBMIT_STACK_KEY: &str = "stk.submitStack";
pub const CLEAN_CLOSED_KEY: &str = "stk.cleanClosed";
pub const MERGE_STRATEGY_KEY: &str = "stk.mergeStrategy";
pub const MERGE_WAIT_KEY: &str = "stk.mergeWait";
pub const SUBMIT_DRAFT_KEY: &str = "stk.submitDraft";
pub const NO_UPDATE_CHECK_KEY: &str = "stk.noUpdateCheck";
pub const ABSORB_INCLUDE_UNSTAGED_KEY: &str = "stk.absorbIncludeUnstaged";
pub const GITHUB_STACKS_KEY: &str = "stk.githubStacks";
pub const GITLAB_HOST_KEY: &str = "stk.gitlabHost";
pub const GITEA_HOST_KEY: &str = "stk.giteaHost";
pub const CHECK_TIMEOUT_KEY: &str = "stk.checkTimeout";
pub const USE_PR_TEMPLATE_KEY: &str = "stk.usePrTemplate";
pub const WORKTREE_DIR_KEY: &str = "stk.worktreeDir";
pub const DEFAULT_REMOTE: &str = "origin";
pub const DEFAULT_CHECK_TIMEOUT_SECS: u64 = 1800;
pub const SETTINGS: &[(&str, &str)] = &[
(PROVIDER_KEY, "auto-detect from the remote URL"),
(REMOTE_KEY, DEFAULT_REMOTE),
(UPDATE_REFS_KEY, "false"),
(FETCH_BEFORE_RESTACK_KEY, "false"),
(PUSH_ON_RESTACK_KEY, "false"),
(PUSH_ON_SUBMIT_KEY, "false"),
(SUBMIT_STACK_KEY, "false"),
(CLEAN_CLOSED_KEY, "false"),
(MERGE_STRATEGY_KEY, "squash"),
(MERGE_WAIT_KEY, "false"),
(SUBMIT_DRAFT_KEY, "false"),
(NO_UPDATE_CHECK_KEY, "false"),
(ABSORB_INCLUDE_UNSTAGED_KEY, "false"),
(GITHUB_STACKS_KEY, "false"),
(GITLAB_HOST_KEY, "none; gitlab.com is always detected"),
(
GITEA_HOST_KEY,
"none; gitea.com and codeberg.org are always detected",
),
(CHECK_TIMEOUT_KEY, "1800 (30m); 0 waits indefinitely"),
(USE_PR_TEMPLATE_KEY, "true"),
(
WORKTREE_DIR_KEY,
"a <repo>-worktrees directory beside the repo",
),
];
pub fn remote() -> Result<String> {
Ok(git::config_get(REMOTE_KEY)?.unwrap_or_else(|| DEFAULT_REMOTE.to_owned()))
}
pub fn gitlab_host() -> Result<Option<String>> {
git::config_get(GITLAB_HOST_KEY)
}
pub fn gitea_host() -> Result<Option<String>> {
git::config_get(GITEA_HOST_KEY)
}
pub fn merge_strategy() -> Result<String> {
let strategy = git::config_get(MERGE_STRATEGY_KEY)?.unwrap_or_else(|| "squash".to_owned());
match strategy.as_str() {
"squash" | "rebase" | "merge" => Ok(strategy),
other => anyhow::bail!(
"unsupported stk.mergeStrategy value {other:?}; expected squash, rebase, or merge"
),
}
}
pub fn check_timeout() -> Result<Option<Duration>> {
parse_check_timeout(git::config_get(CHECK_TIMEOUT_KEY)?.as_deref())
}
fn parse_check_timeout(value: Option<&str>) -> Result<Option<Duration>> {
let seconds = match value {
Some(raw) => raw.trim().parse::<u64>().map_err(|_| {
anyhow::anyhow!(
"invalid {CHECK_TIMEOUT_KEY} value {raw:?}; expected a whole number of seconds"
)
})?,
None => DEFAULT_CHECK_TIMEOUT_SECS,
};
Ok((seconds > 0).then(|| Duration::from_secs(seconds)))
}
pub fn bool_setting(key: &str) -> Result<bool> {
Ok(git::config_get_bool(key)?.unwrap_or(false))
}
pub fn use_pr_template() -> Result<bool> {
Ok(git::config_get_bool(USE_PR_TEMPLATE_KEY)?.unwrap_or(true))
}
pub fn push_enabled(mode: PushMode, key: &str) -> Result<bool> {
match mode {
PushMode::Config => Ok(git::config_get_bool(key)?.unwrap_or(false)),
PushMode::Enabled => Ok(true),
PushMode::Disabled => Ok(false),
}
}
pub fn fetch_enabled(mode: FetchMode) -> Result<bool> {
match mode {
FetchMode::Config => Ok(git::config_get_bool(FETCH_BEFORE_RESTACK_KEY)?.unwrap_or(false)),
FetchMode::Enabled => Ok(true),
FetchMode::Disabled => Ok(false),
}
}
pub fn worktree_dir() -> Result<std::path::PathBuf> {
if let Some(configured) = git::config_get(WORKTREE_DIR_KEY)?
&& !configured.trim().is_empty()
{
return std::path::absolute(configured.trim()).context("failed to resolve stk.worktreeDir");
}
let root = git::main_worktree_root()?;
let name = root
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "repo".to_owned());
let parent = root
.parent()
.context("repository root has no parent directory")?;
Ok(parent.join(format!("{name}-worktrees")))
}
pub fn worktree_path_for(branch: &str) -> Result<std::path::PathBuf> {
let mut path = worktree_dir()?;
for component in branch.split('/').filter(|part| !part.is_empty()) {
if component == "." || component == ".." {
bail!("branch name {branch} cannot be used as a worktree path");
}
path.push(component);
}
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_timeout_defaults_when_unset() {
assert_eq!(
parse_check_timeout(None).unwrap(),
Some(Duration::from_secs(DEFAULT_CHECK_TIMEOUT_SECS))
);
}
#[test]
fn check_timeout_zero_waits_indefinitely() {
assert_eq!(parse_check_timeout(Some("0")).unwrap(), None);
}
#[test]
fn check_timeout_reads_whole_seconds() {
assert_eq!(
parse_check_timeout(Some("300")).unwrap(),
Some(Duration::from_secs(300))
);
assert_eq!(
parse_check_timeout(Some(" 60 ")).unwrap(),
Some(Duration::from_secs(60))
);
}
#[test]
fn check_timeout_rejects_non_numbers() {
let error = parse_check_timeout(Some("soon")).unwrap_err();
assert!(
error.to_string().contains("stk.checkTimeout"),
"unexpected error: {error:#}"
);
}
}