1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
use futures::future::join_all;
use serde::Deserialize;
use std::error::Error;

use crate::{api, Credentials};

#[derive(Deserialize, Debug, Clone)]
pub struct SearchItem {
    url: String,
    title: String,
}

#[derive(Deserialize, Debug, Clone)]
pub struct PullRequestRef {
    label: String,
    #[serde(rename = "ref")]
    gitref: String,
    sha: String,
}

#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PullRequestStatus {
    #[serde(rename = "open")]
    Open,
    #[serde(rename = "closed")]
    Closed,
}

#[derive(Deserialize, Debug, Clone)]
pub struct PullRequest {
    id: usize,
    number: usize,
    head: PullRequestRef,
    base: PullRequestRef,
    title: String,
    url: String,
    body: String,
    state: PullRequestStatus,
}

impl PullRequest {
    pub fn head(&self) -> &str {
        &self.head.gitref
    }

    pub fn base(&self) -> &str {
        &self.base.gitref
    }

    pub fn url(&self) -> &str {
        &self.url
    }

    pub fn number(&self) -> usize {
        self.number
    }

    pub fn title(&self) -> String {
        match &self.state {
            PullRequestStatus::Open => self.title.to_owned(),
            PullRequestStatus::Closed => format!("~~{}~~", &self.title.trim()),
        }
    }

    pub fn state(&self) -> &PullRequestStatus {
        &self.state
    }

    pub fn body(&self) -> &str {
        &self.body
    }

    pub fn note(&self) -> &str {
        match &self.state {
            PullRequestStatus::Open => "N/A",
            PullRequestStatus::Closed => "Merged",
        }
    }
}

#[derive(Deserialize, Debug)]
struct SearchResponse {
    items: Vec<SearchItem>,
}

pub async fn fetch_pull_requests_matching(
    pattern: &str,
    credentials: &Credentials,
) -> Result<Vec<PullRequest>, Box<dyn Error>> {
    let client = reqwest::Client::new();

    let request = api::base_request(
        &client,
        &credentials,
        "https://api.github.com/search/issues",
    )
    .query(&[("q", format!("{} in:title", pattern))]);

    let items = request.send().await?.json::<SearchResponse>().await?.items;

    let item_futures = items.into_iter().map(|item| {
        api::base_request(&client, &credentials, &item.url.replace("issues", "pulls")).send()
    });

    // The `unwrap`s are required here because both `reqwest::send` and `reqwest::json` return a `Result` which has
    // to be unwrapped after the future has been `await`ed on.
    let items = join_all(item_futures)
        .await
        .into_iter()
        .map(|item| item.unwrap());

    let responses: Vec<_> = join_all(items.map(|item| item.json::<PullRequest>()))
        .await
        .into_iter()
        .map(|item| item.unwrap())
        .collect();

    Ok(responses)
}