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_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 anyhow::Context;
12use forgejo_api::structs::{
13    Issue, IssueGetCommentsQuery, IssueListIssuesQuery, IssueListIssuesQueryState,
14};
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) -> anyhow::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        } else {
51            if ctx.global_args.non_interactive {
52                anyhow::bail!("non-interactive mode enabled. You have to specify an issue ID");
53            };
54            let (_, issues_list) = spin_until_ready(
55                ctx.client
56                    .issue_list_issues(
57                        owner.as_str(),
58                        repo.as_str(),
59                        IssueListIssuesQuery {
60                            state: Some(state),
61                            ..Default::default()
62                        },
63                    )
64                    .send(),
65            )
66            .await?;
67
68            fuzzy_select_with_key(&issues_list, select_prompt_for("issue"), display_issue)
69                .cloned()?
70        };
71
72        match ctx.global_args.output_mode {
73            OutputMode::Pretty => {
74                if ctx.args.comments {
75                    spin_until_ready(present_issue_comments(&ctx, &selected_issue)).await?;
76                } else {
77                    present_issue_overview(&ctx, &selected_issue);
78                }
79            }
80            OutputMode::Json => {
81                if ctx.args.comments {
82                    let index = selected_issue
83                        .number
84                        .context("Selected issue has to have a valid issue number")?;
85                    let (_, comments) = ctx
86                        .client
87                        .issue_get_comments(
88                            owner.as_str(),
89                            repo.as_str(),
90                            index,
91                            IssueGetCommentsQuery::default(),
92                        )
93                        .send()
94                        .await?;
95                    comments.print_json()?;
96                } else {
97                    selected_issue.print_json()?;
98                }
99            }
100        }
101
102        Ok(())
103    }
104}
105
106fn present_issue_overview(ctx: &BergContext<ViewIssueArgs>, issue: &Issue) {
107    let days_passed_since_creation =
108        option_display(&issue.created_at.as_ref().map(render_datetime_and_info));
109
110    let mut table = ctx.make_table();
111
112    table
113        .set_header(vec![option_display(
114            &issue.id.as_ref().map(|id| format!("Issue #{}", id)),
115        )])
116        .add_row(vec![String::from("Title"), option_display(&issue.title)])
117        .add_row(vec![String::from("Created"), days_passed_since_creation])
118        .add_row(vec![
119            String::from("Labels"),
120            option_display(&issue.labels.as_ref().map(|labels| {
121                labels
122                    .iter()
123                    .map(|label| option_display(&label.name))
124                    .collect::<Vec<_>>()
125                    .join(", ")
126            })),
127        ])
128        .add_row(vec![
129            String::from("Description"),
130            option_display(&issue.body),
131        ]);
132
133    println!("{table}", table = table.show());
134}
135
136async fn present_issue_comments(
137    ctx: &BergContext<ViewIssueArgs>,
138    issue: &Issue,
139) -> anyhow::Result<()> {
140    let index = issue.number.context("Issue has no issue number")?;
141    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
142    let (header, comments) = {
143        let (_, comments_list) = ctx
144            .client
145            .issue_get_comments(
146                owner.as_str(),
147                repo.as_str(),
148                index,
149                IssueGetCommentsQuery::default(),
150            )
151            .send()
152            .await?;
153        let header = format!(
154            "Issue #{} {}",
155            index,
156            if comments_list.is_empty() {
157                "(no comments)"
158            } else {
159                "comments"
160            }
161        );
162        (header, comments_list)
163    };
164
165    let mut table = ctx.make_table();
166
167    table
168        .add_row(vec![header])
169        .add_rows(comments.into_iter().filter_map(|comment| {
170            let username = comment.user.as_ref()?.login.as_ref()?.as_str();
171            let creation_time = comment.created_at.as_ref()?;
172            let comment = comment.body.as_ref()?;
173            let comment = render_comment(&ctx.config, username, creation_time, comment);
174            Some(vec![comment])
175        }));
176
177    println!("{table}", table = table.show());
178
179    Ok(())
180}