nixpkgs_track_lib/
lib.rs

1use chrono::prelude::*;
2use reqwest::{RequestBuilder, StatusCode};
3use serde::Deserialize;
4
5use thiserror::Error;
6
7const BASE_API_URL: &str = "https://api.github.com/repos/nixos/nixpkgs";
8
9fn build_request(client: impl AsRef<reqwest::Client>, url: &str, token: Option<&str>) -> RequestBuilder {
10	let mut request = client
11		.as_ref()
12		.get(url)
13		.header("User-Agent", "nixpkgs-track");
14
15	if let Some(token) = token {
16		request = request.bearer_auth(token);
17	}
18
19	request
20}
21
22pub async fn fetch_nixpkgs_pull_request(client: impl AsRef<reqwest::Client>, pull_request: u64, token: Option<&str>) -> Result<PullRequest, NixpkgsTrackError> {
23	let url = format!("{BASE_API_URL}/pulls/{pull_request}");
24	let response = build_request(client, &url, token)
25		.send()
26		.await;
27
28	log::debug!("fetch_nixpkgs_pull_request: {:?}", response);
29
30	match response {
31		Ok(response) => match response.status() {
32			StatusCode::OK => response
33				.json::<PullRequest>()
34				.await
35				.map_err(NixpkgsTrackError::RequestFailed),
36			StatusCode::NOT_FOUND => Err(NixpkgsTrackError::PullRequestNotFound(pull_request)),
37			StatusCode::FORBIDDEN => Err(NixpkgsTrackError::RateLimitExceeded),
38			_ => Err(NixpkgsTrackError::RequestFailed(response.error_for_status().unwrap_err())),
39		},
40		Err(err) => Err(NixpkgsTrackError::RequestFailed(err)),
41	}
42}
43
44pub async fn branch_contains_commit(client: impl AsRef<reqwest::Client>, branch: &str, commit: &str, token: Option<&str>) -> Result<bool, NixpkgsTrackError> {
45	// `per_page=1000000&page=100`: a hack for the API, to _not_ return
46	//   information about files or commits, which we do not need here;
47	//   we only need to know whether it's `ahead` or `behind`
48	let url = format!("{BASE_API_URL}/compare/{branch}...{commit}?per_page=1000000&page=100");
49	let response = build_request(client, &url, token)
50		.send()
51		.await;
52
53	log::debug!("branch_contains_commit: {:?}", response);
54
55	match response {
56		Ok(response) => match response.status() {
57			StatusCode::OK => match response.json::<Comparison>().await {
58				Ok(json) => Ok(json.status == "identical" || json.status == "behind"),
59				Err(err) => Err(NixpkgsTrackError::RequestFailed(err)),
60			},
61			StatusCode::NOT_FOUND => Ok(false),
62			StatusCode::FORBIDDEN => Err(NixpkgsTrackError::RateLimitExceeded),
63			_ => Err(NixpkgsTrackError::RequestFailed(response.error_for_status().unwrap_err())),
64		},
65		Err(err) => Err(NixpkgsTrackError::RequestFailed(err)),
66	}
67}
68
69#[derive(Clone, Debug, Deserialize)]
70pub struct User {
71	pub login: String,
72	pub url: String,
73}
74
75#[non_exhaustive]
76#[derive(Clone, Debug, Deserialize)]
77pub struct PullRequest {
78	pub html_url: String,
79	pub number: u64,
80	pub title: String,
81	pub user: User,
82	pub created_at: DateTime<Utc>,
83	pub merged_at: Option<DateTime<Utc>>,
84	pub merged: bool,
85	pub merge_commit_sha: Option<String>,
86	/// Base branch that the pull request is merged into
87	pub base: ForkBranch,
88	/// Head branch that the pull request is merged from
89	pub head: ForkBranch,
90}
91
92#[non_exhaustive]
93#[derive(Clone, Debug, Deserialize)]
94pub struct ForkBranch {
95	/// Fork and branch name in the format "owner:branch"
96	pub label: String,
97	/// Branch name in a given fork
98	pub r#ref: String,
99	pub sha: String,
100}
101
102#[derive(Clone, Debug, Deserialize)]
103pub struct Comparison {
104	pub status: String,
105}
106
107#[derive(Clone, Debug, Deserialize)]
108pub struct GitHubError {
109	pub message: String,
110	pub documentation_url: String,
111}
112
113#[derive(Error, Debug)]
114#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
115pub enum NixpkgsTrackError {
116	#[error("Pull request not found")]
117	PullRequestNotFound(u64),
118	#[error("Rate limit exceeded")]
119	RateLimitExceeded,
120	#[error(transparent)]
121	RequestFailed(#[from] reqwest::Error),
122}