Skip to main content

git_stk/
settings.rs

1//! Every stk-owned git config key and its resolution logic, in one place.
2
3use 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
28/// How long `merge --wait` polls a review's checks before giving up, unless
29/// `stk.checkTimeout` overrides it. Generous so a slow-but-real CI is not cut
30/// off; the point is to bound a pipeline that never settles, not a long one.
31pub const DEFAULT_CHECK_TIMEOUT_SECS: u64 = 1800;
32
33/// Every `[stk]` setting the tool reads, with its default behavior. Shown by
34/// `git stk config`.
35pub 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
57/// The remote used for provider detection, trunk discovery, and pushes.
58pub fn remote() -> Result<String> {
59    Ok(git::config_get(REMOTE_KEY)?.unwrap_or_else(|| DEFAULT_REMOTE.to_owned()))
60}
61
62/// A self-hosted GitLab host (e.g. `gitlab.example.com`) to recognize as
63/// GitLab alongside gitlab.com (`stk.gitlabHost`). `glab` reads the host from
64/// the git remote on its own, so this only widens stk's provider detection.
65pub fn gitlab_host() -> Result<Option<String>> {
66    git::config_get(GITLAB_HOST_KEY)
67}
68
69/// A self-hosted Gitea/Forgejo host (e.g. `gitea.example.com`) to recognize as
70/// Gitea alongside gitea.com and codeberg.org (`stk.giteaHost`). `tea` reads
71/// the host from the git remote itself, so this only widens stk's detection.
72pub fn gitea_host() -> Result<Option<String>> {
73    git::config_get(GITEA_HOST_KEY)
74}
75
76/// The merge strategy for `git stk merge`: squash, rebase, or merge.
77pub 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
87/// How long `merge --wait` keeps polling a review's checks before giving up,
88/// from `stk.checkTimeout` (whole seconds). `0` waits indefinitely; unset uses
89/// [`DEFAULT_CHECK_TIMEOUT_SECS`].
90pub 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    // Zero is the explicit "wait forever" escape hatch.
104    Ok((seconds > 0).then(|| Duration::from_secs(seconds)))
105}
106
107/// A boolean setting's value, defaulting to false when unset.
108pub fn bool_setting(key: &str) -> Result<bool> {
109    Ok(git::config_get_bool(key)?.unwrap_or(false))
110}
111
112/// Whether to seed a new review's body from the repo's PR/MR template
113/// (`stk.usePrTemplate`). Defaults to true - unlike most bool settings - so
114/// the template is honored out of the box; set false to opt into a lean,
115/// git-stk-only body.
116pub fn use_pr_template() -> Result<bool> {
117    Ok(git::config_get_bool(USE_PR_TEMPLATE_KEY)?.unwrap_or(true))
118}
119
120/// Resolve a `--push`/`--no-push` flag pair against its config-key default.
121pub 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
129/// Resolve a `--fetch`/`--no-fetch` flag pair against `stk.fetchBeforeRestack`.
130pub 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        // Surrounding whitespace is tolerated (git config values can carry it).
162        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}