1use std::time::Duration;
4
5use anyhow::{Context, Result, bail};
6
7use crate::cli::{FetchMode, PushMode};
8use crate::git;
9
10pub const PROVIDER_KEY: &str = "stk.provider";
11pub const REMOTE_KEY: &str = "stk.remote";
12pub const UPDATE_REFS_KEY: &str = "stk.updateRefs";
13pub const FETCH_BEFORE_RESTACK_KEY: &str = "stk.fetchBeforeRestack";
14pub const PUSH_ON_RESTACK_KEY: &str = "stk.pushOnRestack";
15pub const PUSH_ON_SUBMIT_KEY: &str = "stk.pushOnSubmit";
16pub const SUBMIT_STACK_KEY: &str = "stk.submitStack";
17pub const CLEAN_CLOSED_KEY: &str = "stk.cleanClosed";
18pub const MERGE_STRATEGY_KEY: &str = "stk.mergeStrategy";
19pub const MERGE_WAIT_KEY: &str = "stk.mergeWait";
20pub const SUBMIT_DRAFT_KEY: &str = "stk.submitDraft";
21pub const NO_UPDATE_CHECK_KEY: &str = "stk.noUpdateCheck";
22pub const ABSORB_INCLUDE_UNSTAGED_KEY: &str = "stk.absorbIncludeUnstaged";
23pub const GITHUB_STACKS_KEY: &str = "stk.githubStacks";
34pub const GITLAB_HOST_KEY: &str = "stk.gitlabHost";
35pub const GITEA_HOST_KEY: &str = "stk.giteaHost";
36pub const CHECK_TIMEOUT_KEY: &str = "stk.checkTimeout";
37pub const USE_PR_TEMPLATE_KEY: &str = "stk.usePrTemplate";
38pub const WORKTREE_DIR_KEY: &str = "stk.worktreeDir";
39pub const DEFAULT_REMOTE: &str = "origin";
40
41pub const DEFAULT_CHECK_TIMEOUT_SECS: u64 = 1800;
45
46pub const SETTINGS: &[(&str, &str)] = &[
49 (PROVIDER_KEY, "auto-detect from the remote URL"),
50 (REMOTE_KEY, DEFAULT_REMOTE),
51 (UPDATE_REFS_KEY, "false"),
52 (FETCH_BEFORE_RESTACK_KEY, "false"),
53 (PUSH_ON_RESTACK_KEY, "false"),
54 (PUSH_ON_SUBMIT_KEY, "false"),
55 (SUBMIT_STACK_KEY, "false"),
56 (CLEAN_CLOSED_KEY, "false"),
57 (MERGE_STRATEGY_KEY, "squash"),
58 (MERGE_WAIT_KEY, "false"),
59 (SUBMIT_DRAFT_KEY, "false"),
60 (NO_UPDATE_CHECK_KEY, "false"),
61 (ABSORB_INCLUDE_UNSTAGED_KEY, "false"),
62 (GITHUB_STACKS_KEY, "false"),
63 (GITLAB_HOST_KEY, "none; gitlab.com is always detected"),
64 (
65 GITEA_HOST_KEY,
66 "none; gitea.com and codeberg.org are always detected",
67 ),
68 (CHECK_TIMEOUT_KEY, "1800 (30m); 0 waits indefinitely"),
69 (USE_PR_TEMPLATE_KEY, "true"),
70 (
71 WORKTREE_DIR_KEY,
72 "a <repo>-worktrees directory beside the repo",
73 ),
74];
75
76pub fn remote() -> Result<String> {
78 Ok(git::config_get(REMOTE_KEY)?.unwrap_or_else(|| DEFAULT_REMOTE.to_owned()))
79}
80
81pub fn gitlab_host() -> Result<Option<String>> {
85 git::config_get(GITLAB_HOST_KEY)
86}
87
88pub fn gitea_host() -> Result<Option<String>> {
92 git::config_get(GITEA_HOST_KEY)
93}
94
95pub fn merge_strategy() -> Result<String> {
97 let strategy = git::config_get(MERGE_STRATEGY_KEY)?.unwrap_or_else(|| "squash".to_owned());
98 match strategy.as_str() {
99 "squash" | "rebase" | "merge" => Ok(strategy),
100 other => anyhow::bail!(
101 "unsupported stk.mergeStrategy value {other:?}; expected squash, rebase, or merge"
102 ),
103 }
104}
105
106pub fn check_timeout() -> Result<Option<Duration>> {
110 parse_check_timeout(git::config_get(CHECK_TIMEOUT_KEY)?.as_deref())
111}
112
113fn parse_check_timeout(value: Option<&str>) -> Result<Option<Duration>> {
114 let seconds = match value {
115 Some(raw) => raw.trim().parse::<u64>().map_err(|_| {
116 anyhow::anyhow!(
117 "invalid {CHECK_TIMEOUT_KEY} value {raw:?}; expected a whole number of seconds"
118 )
119 })?,
120 None => DEFAULT_CHECK_TIMEOUT_SECS,
121 };
122 Ok((seconds > 0).then(|| Duration::from_secs(seconds)))
124}
125
126pub fn bool_setting(key: &str) -> Result<bool> {
128 Ok(git::config_get_bool(key)?.unwrap_or(false))
129}
130
131pub fn use_pr_template() -> Result<bool> {
136 Ok(git::config_get_bool(USE_PR_TEMPLATE_KEY)?.unwrap_or(true))
137}
138
139pub fn push_enabled(mode: PushMode, key: &str) -> Result<bool> {
141 match mode {
142 PushMode::Config => Ok(git::config_get_bool(key)?.unwrap_or(false)),
143 PushMode::Enabled => Ok(true),
144 PushMode::Disabled => Ok(false),
145 }
146}
147
148pub fn fetch_enabled(mode: FetchMode) -> Result<bool> {
150 match mode {
151 FetchMode::Config => Ok(git::config_get_bool(FETCH_BEFORE_RESTACK_KEY)?.unwrap_or(false)),
152 FetchMode::Enabled => Ok(true),
153 FetchMode::Disabled => Ok(false),
154 }
155}
156
157pub fn worktree_dir() -> Result<std::path::PathBuf> {
166 if let Some(configured) = git::config_get(WORKTREE_DIR_KEY)?
167 && !configured.trim().is_empty()
168 {
169 return std::path::absolute(configured.trim()).context("failed to resolve stk.worktreeDir");
170 }
171
172 let root = git::main_worktree_root()?;
173 let name = root
174 .file_name()
175 .map(|name| name.to_string_lossy().into_owned())
176 .unwrap_or_else(|| "repo".to_owned());
177 let parent = root
178 .parent()
179 .context("repository root has no parent directory")?;
180 Ok(parent.join(format!("{name}-worktrees")))
181}
182
183pub fn worktree_path_for(branch: &str) -> Result<std::path::PathBuf> {
188 let mut path = worktree_dir()?;
189 for component in branch.split('/').filter(|part| !part.is_empty()) {
190 if component == "." || component == ".." {
191 bail!("branch name {branch} cannot be used as a worktree path");
192 }
193 path.push(component);
194 }
195 Ok(path)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn check_timeout_defaults_when_unset() {
204 assert_eq!(
205 parse_check_timeout(None).unwrap(),
206 Some(Duration::from_secs(DEFAULT_CHECK_TIMEOUT_SECS))
207 );
208 }
209
210 #[test]
211 fn check_timeout_zero_waits_indefinitely() {
212 assert_eq!(parse_check_timeout(Some("0")).unwrap(), None);
213 }
214
215 #[test]
216 fn check_timeout_reads_whole_seconds() {
217 assert_eq!(
218 parse_check_timeout(Some("300")).unwrap(),
219 Some(Duration::from_secs(300))
220 );
221 assert_eq!(
223 parse_check_timeout(Some(" 60 ")).unwrap(),
224 Some(Duration::from_secs(60))
225 );
226 }
227
228 #[test]
229 fn check_timeout_rejects_non_numbers() {
230 let error = parse_check_timeout(Some("soon")).unwrap_err();
231 assert!(
232 error.to_string().contains("stk.checkTimeout"),
233 "unexpected error: {error:#}"
234 );
235 }
236}