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::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 PUSH_ON_RESTACK_KEY: &str = "stk.pushOnRestack";
14pub const PUSH_ON_SUBMIT_KEY: &str = "stk.pushOnSubmit";
15pub const SUBMIT_STACK_KEY: &str = "stk.submitStack";
16pub const MERGE_STRATEGY_KEY: &str = "stk.mergeStrategy";
17pub const MERGE_WAIT_KEY: &str = "stk.mergeWait";
18pub const SUBMIT_DRAFT_KEY: &str = "stk.submitDraft";
19pub const NO_UPDATE_CHECK_KEY: &str = "stk.noUpdateCheck";
20pub const ABSORB_INCLUDE_UNSTAGED_KEY: &str = "stk.absorbIncludeUnstaged";
21pub const GITLAB_HOST_KEY: &str = "stk.gitlabHost";
22pub const CHECK_TIMEOUT_KEY: &str = "stk.checkTimeout";
23pub const USE_PR_TEMPLATE_KEY: &str = "stk.usePrTemplate";
24pub const DEFAULT_REMOTE: &str = "origin";
25
26/// How long `merge --wait` polls a review's checks before giving up, unless
27/// `stk.checkTimeout` overrides it. Generous so a slow-but-real CI is not cut
28/// off; the point is to bound a pipeline that never settles, not a long one.
29pub const DEFAULT_CHECK_TIMEOUT_SECS: u64 = 1800;
30
31/// Every `[stk]` setting the tool reads, with its default behavior. Shown by
32/// `git stk config`.
33pub const SETTINGS: &[(&str, &str)] = &[
34    (PROVIDER_KEY, "auto-detect from the remote URL"),
35    (REMOTE_KEY, DEFAULT_REMOTE),
36    (UPDATE_REFS_KEY, "false"),
37    (PUSH_ON_RESTACK_KEY, "false"),
38    (PUSH_ON_SUBMIT_KEY, "false"),
39    (SUBMIT_STACK_KEY, "false"),
40    (MERGE_STRATEGY_KEY, "squash"),
41    (MERGE_WAIT_KEY, "false"),
42    (SUBMIT_DRAFT_KEY, "false"),
43    (NO_UPDATE_CHECK_KEY, "false"),
44    (ABSORB_INCLUDE_UNSTAGED_KEY, "false"),
45    (GITLAB_HOST_KEY, "none; gitlab.com is always detected"),
46    (CHECK_TIMEOUT_KEY, "1800 (30m); 0 waits indefinitely"),
47    (USE_PR_TEMPLATE_KEY, "true"),
48];
49
50/// The remote used for provider detection, trunk discovery, and pushes.
51pub fn remote() -> Result<String> {
52    Ok(git::config_get(REMOTE_KEY)?.unwrap_or_else(|| DEFAULT_REMOTE.to_owned()))
53}
54
55/// A self-hosted GitLab host (e.g. `gitlab.example.com`) to recognize as
56/// GitLab alongside gitlab.com (`stk.gitlabHost`). `glab` reads the host from
57/// the git remote on its own, so this only widens stk's provider detection.
58pub fn gitlab_host() -> Result<Option<String>> {
59    git::config_get(GITLAB_HOST_KEY)
60}
61
62/// The merge strategy for `git stk merge`: squash, rebase, or merge.
63pub fn merge_strategy() -> Result<String> {
64    let strategy = git::config_get(MERGE_STRATEGY_KEY)?.unwrap_or_else(|| "squash".to_owned());
65    match strategy.as_str() {
66        "squash" | "rebase" | "merge" => Ok(strategy),
67        other => anyhow::bail!(
68            "unsupported stk.mergeStrategy value {other:?}; expected squash, rebase, or merge"
69        ),
70    }
71}
72
73/// How long `merge --wait` keeps polling a review's checks before giving up,
74/// from `stk.checkTimeout` (whole seconds). `0` waits indefinitely; unset uses
75/// [`DEFAULT_CHECK_TIMEOUT_SECS`].
76pub fn check_timeout() -> Result<Option<Duration>> {
77    parse_check_timeout(git::config_get(CHECK_TIMEOUT_KEY)?.as_deref())
78}
79
80fn parse_check_timeout(value: Option<&str>) -> Result<Option<Duration>> {
81    let seconds = match value {
82        Some(raw) => raw.trim().parse::<u64>().map_err(|_| {
83            anyhow::anyhow!(
84                "invalid {CHECK_TIMEOUT_KEY} value {raw:?}; expected a whole number of seconds"
85            )
86        })?,
87        None => DEFAULT_CHECK_TIMEOUT_SECS,
88    };
89    // Zero is the explicit "wait forever" escape hatch.
90    Ok((seconds > 0).then(|| Duration::from_secs(seconds)))
91}
92
93/// A boolean setting's value, defaulting to false when unset.
94pub fn bool_setting(key: &str) -> Result<bool> {
95    Ok(git::config_get_bool(key)?.unwrap_or(false))
96}
97
98/// Whether to seed a new review's body from the repo's PR/MR template
99/// (`stk.usePrTemplate`). Defaults to true - unlike most bool settings - so
100/// the template is honored out of the box; set false to opt into a lean,
101/// git-stk-only body.
102pub fn use_pr_template() -> Result<bool> {
103    Ok(git::config_get_bool(USE_PR_TEMPLATE_KEY)?.unwrap_or(true))
104}
105
106/// Resolve a `--push`/`--no-push` flag pair against its config-key default.
107pub fn push_enabled(mode: PushMode, key: &str) -> Result<bool> {
108    match mode {
109        PushMode::Config => Ok(git::config_get_bool(key)?.unwrap_or(false)),
110        PushMode::Enabled => Ok(true),
111        PushMode::Disabled => Ok(false),
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn check_timeout_defaults_when_unset() {
121        assert_eq!(
122            parse_check_timeout(None).unwrap(),
123            Some(Duration::from_secs(DEFAULT_CHECK_TIMEOUT_SECS))
124        );
125    }
126
127    #[test]
128    fn check_timeout_zero_waits_indefinitely() {
129        assert_eq!(parse_check_timeout(Some("0")).unwrap(), None);
130    }
131
132    #[test]
133    fn check_timeout_reads_whole_seconds() {
134        assert_eq!(
135            parse_check_timeout(Some("300")).unwrap(),
136            Some(Duration::from_secs(300))
137        );
138        // Surrounding whitespace is tolerated (git config values can carry it).
139        assert_eq!(
140            parse_check_timeout(Some(" 60 ")).unwrap(),
141            Some(Duration::from_secs(60))
142        );
143    }
144
145    #[test]
146    fn check_timeout_rejects_non_numbers() {
147        let error = parse_check_timeout(Some("soon")).unwrap_err();
148        assert!(
149            error.to_string().contains("stk.checkTimeout"),
150            "unexpected error: {error:#}"
151        );
152    }
153}