Skip to main content

git_bot_feedback/comments/
thread_comments.rs

1#[cfg(feature = "pyo3")]
2use pyo3::prelude::*;
3
4use super::DEFAULT_MARKER;
5
6/// An enumeration of possible type of comments being posted.
7///
8/// The default is [`CommentKind::Concerns`].
9#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
10#[cfg_attr(feature = "pyo3", pyclass(module = "git_bot_feedback", from_py_object))]
11pub enum CommentKind {
12    /// A comment that admonishes concerns for end-users' attention.
13    #[default]
14    Concerns,
15
16    /// A comment that basically says "Looks Good To Me".
17    Lgtm,
18}
19
20/// An enumeration of supported behaviors about posting comments.
21///
22/// See [`ThreadCommentOptions::policy`](crate::ThreadCommentOptions::policy).
23#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
24#[cfg_attr(feature = "pyo3", pyclass(module = "git_bot_feedback", from_py_object))]
25pub enum CommentPolicy {
26    /// Each thread comment is posted as a new comment.
27    ///
28    /// This may result in perceivable spam because
29    /// every new comment may cause notification emails.
30    Anew,
31
32    /// Like [`CommentPolicy::Anew`], but updates a single comment.
33    ///
34    /// Typically, this is the desirable option when posting thread comments.
35    #[default]
36    Update,
37}
38
39/// Options that control posting comments on a thread.
40#[derive(Debug, Clone)]
41#[cfg_attr(
42    feature = "pyo3",
43    pyclass(module = "git_bot_feedback", from_py_object, get_all, set_all)
44)]
45pub struct ThreadCommentOptions {
46    /// Controls posting comments on a thread that concerns a Pull Request or Push event.
47    ///
48    /// Typically, this is only desirable for Pull Requests.
49    pub policy: CommentPolicy,
50
51    /// The comment to post.
52    pub comment: String,
53
54    /// The [`CommentKind`] that describes the comment's purpose.
55    pub kind: CommentKind,
56
57    /// A string used to mark/identify the thread's comment as a comment submitted by this software.
58    ///
59    /// User comments may be indistinguishable from bot/generated comments if
60    /// this value is not unique enough.
61    ///
62    /// If the git server employs Markdown syntax for comments, then
63    /// it is recommended to set this to an HTML comment that is unique to
64    /// your CI application:
65    ///
66    /// ``<!-- my-cool-CI-app-name -->``
67    ///
68    /// The default value for this is an HTML comment generated from
69    /// this crate's name and version along with the compile-tome's datetime.
70    /// For example:
71    ///
72    /// ``<!-- git-bot-feedback/0.1.0/Jul-14-2025_17-00 -->``
73    pub marker: String,
74
75    /// Disallow posting "Looks Good To Me" comments.
76    ///
77    /// Setting this option to `true` may instigate the deletion of old bot comment(s),
78    /// if any exist.
79    pub no_lgtm: bool,
80}
81
82impl Default for ThreadCommentOptions {
83    fn default() -> Self {
84        Self {
85            policy: Default::default(),
86            comment: Default::default(),
87            kind: Default::default(),
88            marker: DEFAULT_MARKER.to_string(),
89            no_lgtm: Default::default(),
90        }
91    }
92}
93
94impl ThreadCommentOptions {
95    /// Ensure that the [`ThreadCommentOptions::comment`] is marked with
96    /// the [`ThreadCommentOptions::marker`].
97    ///
98    /// Typically only used by implementations of
99    /// [`RestApiClient::post_thread_comment`](crate::client::RestApiClient::post_thread_comment)
100    /// and [`RestApiClient::append_step_summary`](crate::client::RestApiClient::append_step_summary).
101    pub fn mark_comment(&self) -> String {
102        if !self.comment.starts_with(&self.marker) {
103            return format!("{}{}", self.marker, self.comment);
104        }
105        self.comment.clone()
106    }
107}
108
109#[cfg(feature = "pyo3")]
110#[pymethods]
111impl ThreadCommentOptions {
112    /// Create a new instance of ``ThreadCommentOptions``.
113    #[new]
114    #[pyo3(
115        signature = (
116            policy = None,
117            comment = None,
118            kind = None,
119            marker = None,
120            no_lgtm = None,
121        ),
122        text_signature = "(policy: CommentPolicy | None = None, comment: str | None = None, kind: CommentKind | None = None, marker: str | None = None, no_lgtm: bool = False)",
123    )]
124    pub fn new(
125        policy: Option<CommentPolicy>,
126        comment: Option<String>,
127        kind: Option<CommentKind>,
128        marker: Option<String>,
129        no_lgtm: Option<bool>,
130    ) -> Self {
131        Self {
132            policy: policy.unwrap_or_default(),
133            comment: comment.unwrap_or_default(),
134            kind: kind.unwrap_or_default(),
135            marker: marker.unwrap_or_else(|| DEFAULT_MARKER.to_string()),
136            no_lgtm: no_lgtm.unwrap_or_default(),
137        }
138    }
139}
140
141#[cfg(test)]
142mod test {
143    #![allow(clippy::unwrap_used)]
144
145    use super::{DEFAULT_MARKER, ThreadCommentOptions};
146    use chrono::NaiveDateTime;
147
148    #[test]
149    fn default_marker() {
150        let mut opts = ThreadCommentOptions::default();
151        assert_eq!(opts.marker, DEFAULT_MARKER);
152        let datetime_start = concat!(
153            "<!-- ",
154            env!("CARGO_CRATE_NAME"),
155            "/",
156            env!("CARGO_PKG_VERSION"),
157            "/",
158        )
159        .len();
160        let datetime_end = DEFAULT_MARKER.len() - 5;
161        let datetime_str = &DEFAULT_MARKER[datetime_start..datetime_end];
162        NaiveDateTime::parse_from_str(datetime_str, "%b-%d-%Y_%H-%M").unwrap();
163        assert_eq!(opts.mark_comment(), DEFAULT_MARKER);
164        let comment = format!("{DEFAULT_MARKER}Some text data.");
165        opts.comment = comment.clone();
166        assert_eq!(opts.mark_comment(), comment);
167    }
168}