1use std::time::Duration;
4
5use anyhow::Result;
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 MERGE_STRATEGY_KEY: &str = "stk.mergeStrategy";
18pub const MERGE_WAIT_KEY: &str = "stk.mergeWait";
19pub const SUBMIT_DRAFT_KEY: &str = "stk.submitDraft";
20pub const NO_UPDATE_CHECK_KEY: &str = "stk.noUpdateCheck";
21pub const ABSORB_INCLUDE_UNSTAGED_KEY: &str = "stk.absorbIncludeUnstaged";
22pub const GITLAB_HOST_KEY: &str = "stk.gitlabHost";
23pub const GITEA_HOST_KEY: &str = "stk.giteaHost";
24pub const CHECK_TIMEOUT_KEY: &str = "stk.checkTimeout";
25pub const USE_PR_TEMPLATE_KEY: &str = "stk.usePrTemplate";
26pub const DEFAULT_REMOTE: &str = "origin";
27
28pub const DEFAULT_CHECK_TIMEOUT_SECS: u64 = 1800;
32
33pub const SETTINGS: &[(&str, &str)] = &[
36 (PROVIDER_KEY, "auto-detect from the remote URL"),
37 (REMOTE_KEY, DEFAULT_REMOTE),
38 (UPDATE_REFS_KEY, "false"),
39 (FETCH_BEFORE_RESTACK_KEY, "false"),
40 (PUSH_ON_RESTACK_KEY, "false"),
41 (PUSH_ON_SUBMIT_KEY, "false"),
42 (SUBMIT_STACK_KEY, "false"),
43 (MERGE_STRATEGY_KEY, "squash"),
44 (MERGE_WAIT_KEY, "false"),
45 (SUBMIT_DRAFT_KEY, "false"),
46 (NO_UPDATE_CHECK_KEY, "false"),
47 (ABSORB_INCLUDE_UNSTAGED_KEY, "false"),
48 (GITLAB_HOST_KEY, "none; gitlab.com is always detected"),
49 (
50 GITEA_HOST_KEY,
51 "none; gitea.com and codeberg.org are always detected",
52 ),
53 (CHECK_TIMEOUT_KEY, "1800 (30m); 0 waits indefinitely"),
54 (USE_PR_TEMPLATE_KEY, "true"),
55];
56
57pub fn remote() -> Result<String> {
59 Ok(git::config_get(REMOTE_KEY)?.unwrap_or_else(|| DEFAULT_REMOTE.to_owned()))
60}
61
62pub fn gitlab_host() -> Result<Option<String>> {
66 git::config_get(GITLAB_HOST_KEY)
67}
68
69pub fn gitea_host() -> Result<Option<String>> {
73 git::config_get(GITEA_HOST_KEY)
74}
75
76pub fn merge_strategy() -> Result<String> {
78 let strategy = git::config_get(MERGE_STRATEGY_KEY)?.unwrap_or_else(|| "squash".to_owned());
79 match strategy.as_str() {
80 "squash" | "rebase" | "merge" => Ok(strategy),
81 other => anyhow::bail!(
82 "unsupported stk.mergeStrategy value {other:?}; expected squash, rebase, or merge"
83 ),
84 }
85}
86
87pub fn check_timeout() -> Result<Option<Duration>> {
91 parse_check_timeout(git::config_get(CHECK_TIMEOUT_KEY)?.as_deref())
92}
93
94fn parse_check_timeout(value: Option<&str>) -> Result<Option<Duration>> {
95 let seconds = match value {
96 Some(raw) => raw.trim().parse::<u64>().map_err(|_| {
97 anyhow::anyhow!(
98 "invalid {CHECK_TIMEOUT_KEY} value {raw:?}; expected a whole number of seconds"
99 )
100 })?,
101 None => DEFAULT_CHECK_TIMEOUT_SECS,
102 };
103 Ok((seconds > 0).then(|| Duration::from_secs(seconds)))
105}
106
107pub fn bool_setting(key: &str) -> Result<bool> {
109 Ok(git::config_get_bool(key)?.unwrap_or(false))
110}
111
112pub fn use_pr_template() -> Result<bool> {
117 Ok(git::config_get_bool(USE_PR_TEMPLATE_KEY)?.unwrap_or(true))
118}
119
120pub fn push_enabled(mode: PushMode, key: &str) -> Result<bool> {
122 match mode {
123 PushMode::Config => Ok(git::config_get_bool(key)?.unwrap_or(false)),
124 PushMode::Enabled => Ok(true),
125 PushMode::Disabled => Ok(false),
126 }
127}
128
129pub fn fetch_enabled(mode: FetchMode) -> Result<bool> {
131 match mode {
132 FetchMode::Config => Ok(git::config_get_bool(FETCH_BEFORE_RESTACK_KEY)?.unwrap_or(false)),
133 FetchMode::Enabled => Ok(true),
134 FetchMode::Disabled => Ok(false),
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn check_timeout_defaults_when_unset() {
144 assert_eq!(
145 parse_check_timeout(None).unwrap(),
146 Some(Duration::from_secs(DEFAULT_CHECK_TIMEOUT_SECS))
147 );
148 }
149
150 #[test]
151 fn check_timeout_zero_waits_indefinitely() {
152 assert_eq!(parse_check_timeout(Some("0")).unwrap(), None);
153 }
154
155 #[test]
156 fn check_timeout_reads_whole_seconds() {
157 assert_eq!(
158 parse_check_timeout(Some("300")).unwrap(),
159 Some(Duration::from_secs(300))
160 );
161 assert_eq!(
163 parse_check_timeout(Some(" 60 ")).unwrap(),
164 Some(Duration::from_secs(60))
165 );
166 }
167
168 #[test]
169 fn check_timeout_rejects_non_numbers() {
170 let error = parse_check_timeout(Some("soon")).unwrap_err();
171 assert!(
172 error.to_string().contains("stk.checkTimeout"),
173 "unexpected error: {error:#}"
174 );
175 }
176}