codeberg_cli/actions/issue/
view.rs

1use crate::render::comment::render_comment;
2use crate::render::datetime::render_datetime_and_info;
3use crate::render::json::JsonToStdout;
4use crate::render::option::{option_debug_display, option_display};
5use crate::render::spinner::spin_until_ready;
6use crate::render::ui::fuzzy_select_with_key;
7use crate::types::api::state_type::ViewStateType;
8use crate::types::context::BergContext;
9use crate::types::output::OutputMode;
10use crate::{actions::GlobalArgs, types::git::OwnerRepo};
11use forgejo_api::structs::{
12    Issue, IssueGetCommentsQuery, IssueListIssuesQuery, IssueListIssuesQueryState,
13};
14use miette::{Context, IntoDiagnostic};
15
16use crate::actions::text_manipulation::select_prompt_for;
17
18use super::display_issue;
19
20use clap::Parser;
21
22/// View details of selected issue
23#[derive(Parser, Debug)]
24pub struct ViewIssueArgs {
25    /// Optionally, specify a selected issue in the command line
26    pub index: Option<i64>,
27
28    /// Select from issues with the chosen state
29    #[arg(short, long, value_enum, default_value_t = ViewStateType::All)]
30    pub state: ViewStateType,
31
32    /// Disabled: display issue summary | Enabled: display issue comment history
33    #[arg(short, long)]
34    pub comments: bool,
35}
36
37impl ViewIssueArgs {
38    pub async fn run(self, global_args: GlobalArgs) -> miette::Result<()> {
39        let ctx = BergContext::new(self, global_args).await?;
40        let OwnerRepo { repo, owner } = ctx.owner_repo()?;
41        let state = match ctx.args.state {
42            ViewStateType::Closed => IssueListIssuesQueryState::Closed,
43            ViewStateType::Open => IssueListIssuesQueryState::Open,
44            ViewStateType::All => IssueListIssuesQueryState::All,
45        };
46        let selected_issue = if let Some(index) = ctx.args.index {
47            ctx.client
48                .issue_get_issue(owner.as_str(), repo.as_str(), index)
49                .await
50                .into_diagnostic()?
51        } else {
52            if ctx.global_args.non_interactive {
53                miette::bail!("non-interactive mode enabled. You have to specify an issue #number");
54            };
55            let (_, issues_list) = spin_until_ready(
56                ctx.client
57                    .issue_list_issues(
58                        owner.as_str(),
59                        repo.as_str(),
60                        IssueListIssuesQuery {
61                            state: Some(state),
62                            ..Default::default()
63                        },
64                    )
65                    .send(),
66            )
67            .await
68            .into_diagnostic()?;
69
70            fuzzy_select_with_key(&issues_list, select_prompt_for("issue"), display_issue)
71                .cloned()?
72        };
73
74        match ctx.global_args.output_mode {
75            OutputMode::Pretty => {
76                if ctx.args.comments {
77                    spin_until_ready(present_issue_comments(&ctx, &selected_issue)).await?;
78                } else {
79                    present_issue_overview(&ctx, &selected_issue);
80                }
81            }
82            OutputMode::Json => {
83                if ctx.args.comments {
84                    let index = selected_issue
85                        .number
86                        .context("Selected issue has to have a valid issue number")?;
87                    let (_, comments) = ctx
88                        .client
89                        .issue_get_comments(
90                            owner.as_str(),
91                            repo.as_str(),
92                            index,
93                            IssueGetCommentsQuery::default(),
94                        )
95                        .send()
96                        .await
97                        .into_diagnostic()?;
98                    comments.print_json()?;
99                } else {
100                    selected_issue.print_json()?;
101                }
102            }
103        }
104
105        Ok(())
106    }
107}
108
109fn present_issue_overview(ctx: &BergContext<ViewIssueArgs>, issue: &Issue) {
110    let days_passed_since_creation =
111        option_display(&issue.created_at.as_ref().map(render_datetime_and_info));
112
113    let table = ctx
114        .make_table()
115        .set_header(vec![option_display(
116            &issue.id.as_ref().map(|id| format!("Issue #{}", id)),
117        )])
118        .add_row(vec![String::from("Title"), option_display(&issue.title)])
119        .add_row(vec![String::from("Created"), days_passed_since_creation])
120        .add_row(vec![
121            String::from("Labels"),
122            option_display(&issue.labels.as_ref().map(|labels| {
123                labels
124                    .iter()
125                    .map(|label| option_display(&label.name))
126                    .collect::<Vec<_>>()
127                    .join(", ")
128            })),
129        ])
130        .add_row(vec![
131            String::from("State"),
132            option_debug_display(&issue.state),
133        ])
134        .add_row(vec![
135            String::from("Description"),
136            option_display(&issue.body),
137        ]);
138
139    println!("{table}", table = table.show());
140}
141
142async fn present_issue_comments(
143    ctx: &BergContext<ViewIssueArgs>,
144    issue: &Issue,
145) -> miette::Result<()> {
146    let index = issue.number.context("Issue has no issue number")?;
147    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
148    let (header, comments) = {
149        let (_, comments_list) = ctx
150            .client
151            .issue_get_comments(
152                owner.as_str(),
153                repo.as_str(),
154                index,
155                IssueGetCommentsQuery::default(),
156            )
157            .send()
158            .await
159            .into_diagnostic()?;
160        let header = format!(
161            "Issue #{} {}",
162            index,
163            if comments_list.is_empty() {
164                "(no comments)"
165            } else {
166                "comments"
167            }
168        );
169        (header, comments_list)
170    };
171
172    let table = ctx
173        .make_table()
174        .add_row(vec![header])
175        .add_rows(comments.into_iter().filter_map(|comment| {
176            let username = comment.user.as_ref()?.login.as_ref()?.as_str();
177            let creation_time = comment.created_at.as_ref()?;
178            let comment = comment.body.as_ref()?;
179            let comment = render_comment(username, creation_time, comment);
180            Some(vec![comment])
181        }));
182
183    println!("{table}", table = table.show());
184
185    Ok(())
186}