Skip to main content

git_lfs_creds/
chain.rs

1//! Run a sequence of helpers, passing successes/failures back to all of them.
2//!
3//! Mirrors upstream's `CredentialHelpers`: the first helper to return
4//! creds wins for `fill`; `approve`/`reject` are broadcast to every
5//! helper so caches stay in sync with the upstream source of truth
6//! (`git credential`).
7
8use std::io::Write as _;
9
10use crate::helper::{Credentials, Helper, HelperError};
11use crate::query::Query;
12
13/// Try each helper in order on `fill`, broadcast `approve` / `reject`.
14///
15/// Typical wiring puts a [`CachingHelper`] before a
16/// [`GitCredentialHelper`]: the cache short-circuits the slow
17/// shell-out path once a working pair has resolved, and approvals
18/// propagate so subsequent calls hit the cache.
19///
20/// [`CachingHelper`]: crate::CachingHelper
21/// [`GitCredentialHelper`]: crate::GitCredentialHelper
22pub struct HelperChain {
23    helpers: Vec<Box<dyn Helper>>,
24}
25
26impl HelperChain {
27    /// Build a chain from a list of boxed helpers, applied in order.
28    pub fn new(helpers: Vec<Box<dyn Helper>>) -> Self {
29        Self { helpers }
30    }
31
32    /// `true` if no helpers are configured.
33    ///
34    /// Calls into [`Helper::fill`] will always return `Ok(None)` for
35    /// an empty chain.
36    pub fn is_empty(&self) -> bool {
37        self.helpers.is_empty()
38    }
39}
40
41impl Helper for HelperChain {
42    /// Walk helpers in order. The first to return creds wins; helpers
43    /// that error out are logged and skipped so a busted askpass program
44    /// can't lock the user out of `git credential` further down the
45    /// chain. Mirrors upstream's `CredentialHelpers.Fill` at
46    /// `creds/creds.go:502`. If nothing returned creds and at least one
47    /// helper errored, surface the last error so callers see *why*
48    /// nothing worked rather than a bare "credentials not found".
49    fn fill(&self, query: &Query) -> Result<Option<Credentials>, HelperError> {
50        let mut last_err: Option<HelperError> = None;
51        for h in &self.helpers {
52            match h.fill(query) {
53                Ok(Some(c)) => return Ok(Some(c)),
54                Ok(None) => continue,
55                Err(e) => {
56                    // Upstream's `credential fill error: <err>` trace
57                    // at `creds/creds.go:513`. Always-on; `GIT_TRACE`
58                    // gating isn't worth the extra branch for a path
59                    // that already only fires when something failed.
60                    let mut err = std::io::stderr().lock();
61                    let _ = writeln!(err, "credential fill error: {e}");
62                    last_err = Some(e);
63                    continue;
64                }
65            }
66        }
67        match last_err {
68            Some(e) => Err(e),
69            None => Ok(None),
70        }
71    }
72
73    /// Broadcast to every helper. Errors from individual helpers are
74    /// surfaced (first wins) — a failed approve generally means we
75    /// couldn't write to the keystore, which is worth knowing about.
76    fn approve(&self, query: &Query, creds: &Credentials) -> Result<(), HelperError> {
77        let mut first_err = None;
78        for h in &self.helpers {
79            if let Err(e) = h.approve(query, creds) {
80                first_err.get_or_insert(e);
81            }
82        }
83        match first_err {
84            Some(e) => Err(e),
85            None => Ok(()),
86        }
87    }
88
89    fn reject(&self, query: &Query, creds: &Credentials) -> Result<(), HelperError> {
90        let mut first_err = None;
91        for h in &self.helpers {
92            if let Err(e) = h.reject(query, creds) {
93                first_err.get_or_insert(e);
94            }
95        }
96        match first_err {
97            Some(e) => Err(e),
98            None => Ok(()),
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use std::sync::Mutex;
107
108    #[derive(Default)]
109    struct StaticHelper {
110        answer: Option<Credentials>,
111        approves: Mutex<Vec<(Query, Credentials)>>,
112        rejects: Mutex<Vec<(Query, Credentials)>>,
113    }
114
115    impl Helper for StaticHelper {
116        fn fill(&self, _q: &Query) -> Result<Option<Credentials>, HelperError> {
117            Ok(self.answer.clone())
118        }
119        fn approve(&self, q: &Query, c: &Credentials) -> Result<(), HelperError> {
120            self.approves.lock().unwrap().push((q.clone(), c.clone()));
121            Ok(())
122        }
123        fn reject(&self, q: &Query, c: &Credentials) -> Result<(), HelperError> {
124            self.rejects.lock().unwrap().push((q.clone(), c.clone()));
125            Ok(())
126        }
127    }
128
129    fn q() -> Query {
130        Query {
131            protocol: "https".into(),
132            host: "h".into(),
133            path: String::new(),
134        }
135    }
136
137    #[test]
138    fn fill_returns_first_match() {
139        let chain = HelperChain::new(vec![
140            Box::new(StaticHelper {
141                answer: None,
142                ..Default::default()
143            }),
144            Box::new(StaticHelper {
145                answer: Some(Credentials::new("u", "p")),
146                ..Default::default()
147            }),
148        ]);
149        assert_eq!(chain.fill(&q()).unwrap(), Some(Credentials::new("u", "p")));
150    }
151
152    #[test]
153    fn approve_broadcasts_to_all_helpers() {
154        let chain = crate::CachingHelper::new();
155        let outer = HelperChain::new(vec![
156            Box::new(StaticHelper::default()),
157            Box::new(crate::CachingHelper::new()),
158        ]);
159        let c = Credentials::new("u", "p");
160        outer.approve(&q(), &c).unwrap();
161        // First helper recorded the approve; can't peek at the inner cache
162        // through the trait, but the broadcast itself completing without
163        // error is what we're checking.
164        let _ = chain;
165    }
166}