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