A good place?
This commit is contained in:
parent
9cdbc9e2db
commit
4833cb7b0e
10 changed files with 236 additions and 90 deletions
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
# TODO: cannot use mold due to libssl
|
||||
# rustflags = ["-C", "link-arg=-fuse-ld=/nix/store/qcwcg3q98cm6pvyfnpn4apfh5w3xvy1d-mold-2.4.1/bin/mold"]
|
||||
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug",
|
||||
"program": "${workspaceFolder}/target/debug/tasks",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
13
Cargo.toml
13
Cargo.toml
|
@ -17,3 +17,16 @@ serde_json = "1.0.114"
|
|||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
|
|
@ -41,6 +41,14 @@
|
|||
rustPackages.clippy
|
||||
rust-analyzer
|
||||
|
||||
# linker
|
||||
clang
|
||||
mold
|
||||
|
||||
# debugger
|
||||
lldb
|
||||
|
||||
# libs
|
||||
openssl
|
||||
pkg-config
|
||||
];
|
||||
|
|
13
src/client.rs
Normal file
13
src/client.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use reqwest::Client;
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
use reqwest_tracing::TracingMiddleware;
|
||||
|
||||
pub fn wrap_with_middleware(client: Client) -> ClientWithMiddleware {
|
||||
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
|
||||
|
||||
ClientBuilder::new(client)
|
||||
.with(TracingMiddleware::default())
|
||||
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
|
||||
.build()
|
||||
}
|
|
@ -1,59 +1,16 @@
|
|||
use color_eyre::owo_colors::OwoColorize;
|
||||
use crate::result::Result;
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderValue},
|
||||
Client, Url,
|
||||
};
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
use reqwest_tracing::TracingMiddleware;
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use serde::Deserialize;
|
||||
use tracing::trace;
|
||||
|
||||
pub struct GitLab {
|
||||
url: Url,
|
||||
client: ClientWithMiddleware,
|
||||
}
|
||||
|
||||
// ---
|
||||
/* "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
|
||||
// "web_url": "http://localhost:3000/john_smith",
|
||||
// "created_at": "2012-05-23T08:00:58Z",
|
||||
// "bio": "",
|
||||
// "location": null,
|
||||
// "skype": "",
|
||||
// "linkedin": "",
|
||||
// "twitter": "",
|
||||
// "discord": "",
|
||||
// "website_url": "",
|
||||
// "organization": "",
|
||||
// "job_title": "",
|
||||
// "pronouns": "he/him",
|
||||
// "bot": false,
|
||||
// "work_information": null,
|
||||
// "followers": 0,
|
||||
// "following": 0,
|
||||
// "local_time": "3:38 PM",
|
||||
// "last_sign_in_at": "2012-06-01T11:41:01Z",
|
||||
// "confirmed_at": "2012-05-23T09:05:22Z",
|
||||
// "theme_id": 1,
|
||||
// "last_activity_on": "2012-05-23",
|
||||
// "color_scheme_id": 2,
|
||||
// "projects_limit": 100,
|
||||
// "current_sign_in_at": "2012-06-02T06:36:55Z",
|
||||
// "identities": [
|
||||
// {"provider": "github", "extern_uid": "2435223452345"},
|
||||
// {"provider": "bitbucket", "extern_uid": "john_smith"},
|
||||
// {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"}
|
||||
// ],
|
||||
// "can_create_group": true,
|
||||
// "can_create_project": true,
|
||||
// "two_factor_enabled": true,
|
||||
// "external": false,
|
||||
// "private_profile": false,
|
||||
// "commit_email": "admin@example.com",
|
||||
// }
|
||||
*/
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct User {
|
||||
pub id: u64,
|
||||
|
@ -67,35 +24,25 @@ pub struct User {
|
|||
}
|
||||
|
||||
impl GitLab {
|
||||
pub fn try_new(url: &str, token: &str) -> Result<Self, anyhow::Error> {
|
||||
let url = Url::parse(url)?;
|
||||
pub fn try_new(url: &str, token: &str) -> Result<Self> {
|
||||
let url = Url::parse(&format!("{}{}", url.trim_end_matches('/'), "/"))?;
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("PRIVATE-TOKEN", HeaderValue::from_str(token)?);
|
||||
let proxy = reqwest::Proxy::all("socks5h://localhost:9982")?;
|
||||
headers.insert("content-type", HeaderValue::from_str("application/json")?);
|
||||
headers.insert("accepts", HeaderValue::from_str("application/json")?);
|
||||
|
||||
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
|
||||
let base_client = Client::builder()
|
||||
.default_headers(headers)
|
||||
.proxy(proxy)
|
||||
.build()?;
|
||||
let client = ClientBuilder::new(base_client)
|
||||
.with(TracingMiddleware::default())
|
||||
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
|
||||
.build();
|
||||
let base_client = Client::builder().default_headers(headers).build()?;
|
||||
let client = crate::client::wrap_with_middleware(base_client);
|
||||
|
||||
Ok(Self { url, client })
|
||||
}
|
||||
|
||||
pub fn url(&self, path: &str) -> Result<Url, anyhow::Error> {
|
||||
Ok(self.url.join(path)?)
|
||||
pub fn url(&self, path: &str) -> Result<Url> {
|
||||
Ok(self.url.join(path.trim_start_matches('/'))?)
|
||||
}
|
||||
|
||||
pub async fn me(&self) -> Result<User, anyhow::Error> {
|
||||
pub async fn me(&self) -> Result<User> {
|
||||
let res = self.client.get(self.url("/user")?).send().await?;
|
||||
trace!("{res:?}");
|
||||
let body = res.text().await?;
|
||||
trace!("{body:?}");
|
||||
Ok(serde_json::from_str::<User>(&body)?)
|
||||
// Ok(res.json::<User>().await?)
|
||||
Ok(res.json().await?)
|
||||
}
|
||||
}
|
||||
|
|
116
src/jira.rs
116
src/jira.rs
|
@ -1 +1,117 @@
|
|||
use crate::result::Result;
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderValue},
|
||||
Client, Url,
|
||||
};
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tracing::debug;
|
||||
|
||||
pub struct Jira {
|
||||
url: Url,
|
||||
client: ClientWithMiddleware,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct User {
|
||||
pub account_id: String,
|
||||
pub email_address: String,
|
||||
pub account_type: String,
|
||||
pub display_name: String,
|
||||
pub active: bool,
|
||||
pub time_zone: String,
|
||||
pub locale: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Issue {
|
||||
pub id: String,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IssueSearch {
|
||||
issues: Vec<Issue>,
|
||||
max_results: usize,
|
||||
pub start_at: usize,
|
||||
total: usize,
|
||||
}
|
||||
|
||||
impl Jira {
|
||||
pub fn try_new(url: &str, token: &str) -> Result<Self> {
|
||||
let url = Url::parse(&format!("{}{}", url.trim_end_matches('/'), "/"))?;
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("Basic {}", token.trim()))?,
|
||||
);
|
||||
headers.insert("content-type", HeaderValue::from_str("application/json")?);
|
||||
headers.insert("accepts", HeaderValue::from_str("application/json")?);
|
||||
|
||||
let base_client = Client::builder().default_headers(headers).build()?;
|
||||
let client = crate::client::wrap_with_middleware(base_client);
|
||||
|
||||
Ok(Self { url, client })
|
||||
}
|
||||
|
||||
pub fn url(&self, path: &str) -> Result<Url> {
|
||||
Ok(self.url.join(path.trim_start_matches('/'))?)
|
||||
}
|
||||
|
||||
pub async fn me(&self) -> Result<User> {
|
||||
let res = self
|
||||
.client
|
||||
.get(self.url("/rest/api/3/myself")?)
|
||||
.send()
|
||||
.await?;
|
||||
Ok(res.json().await?)
|
||||
}
|
||||
|
||||
pub async fn jql(&self, jql: &str) -> Result<Vec<Issue>> {
|
||||
debug!("Fetching issues for jql {}", jql);
|
||||
let mut results = self
|
||||
.client
|
||||
.get(self.url("/rest/api/3/search")?)
|
||||
.query(&[("jql", jql)])
|
||||
.send()
|
||||
.await?
|
||||
.json::<IssueSearch>()
|
||||
.await?;
|
||||
|
||||
// TODO: parallelize after first page
|
||||
// TODO: are there rate limits to be concerned about
|
||||
|
||||
let mut issues = Vec::with_capacity(results.total);
|
||||
issues.append(&mut results.issues);
|
||||
|
||||
let mut futures = Vec::with_capacity((results.total / results.max_results) + 1);
|
||||
|
||||
for i in 1..futures.capacity() {
|
||||
let start_at = i * results.max_results;
|
||||
debug!("Fetching issues for jql {} (page {})", jql, i);
|
||||
futures.push(spawn(
|
||||
self.client
|
||||
.get(self.url("/rest/api/3/search")?)
|
||||
.query(&[("jql", jql), ("startAt", &start_at.to_string())])
|
||||
.send(),
|
||||
));
|
||||
}
|
||||
|
||||
for task in futures {
|
||||
let mut result = task.await.unwrap()?.json::<IssueSearch>().await?;
|
||||
issues.append(&mut result.issues);
|
||||
}
|
||||
|
||||
Ok(issues)
|
||||
}
|
||||
|
||||
pub async fn assigned_open_issues(&self) -> Result<Vec<Issue>> {
|
||||
let me = self.me().await?;
|
||||
let jql = format!("assignee = {0} and statusCategory != Done", me.account_id);
|
||||
self.jql(&jql).await
|
||||
}
|
||||
}
|
||||
|
|
47
src/main.rs
47
src/main.rs
|
@ -1,37 +1,44 @@
|
|||
mod client;
|
||||
mod config;
|
||||
mod gitlab;
|
||||
mod jira;
|
||||
mod result;
|
||||
mod tasks;
|
||||
|
||||
use crate::result::Result;
|
||||
use tasks::Tasks;
|
||||
use tracing::{info, instrument};
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
|
||||
|
||||
const ANSI_CLEAR: &'static str = "\x1b[2J\x1b[1;1H";
|
||||
|
||||
mod config;
|
||||
mod gitlab;
|
||||
mod jira;
|
||||
mod tasks;
|
||||
|
||||
#[instrument]
|
||||
pub fn init() {
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
color_eyre::install().expect("Failed to install color_eyre");
|
||||
setup_trace_logger();
|
||||
info!("Instrumentation initialized.");
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub fn setup_trace_logger() {
|
||||
let filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::TRACE.into())
|
||||
.parse_lossy("trace,tasks=trace");
|
||||
.parse_lossy("info,tasks=trace");
|
||||
|
||||
tracing_subscriber::fmt().with_env_filter(filter).init();
|
||||
|
||||
match run().await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) => {
|
||||
error!("{err}");
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
init();
|
||||
|
||||
tracing::info!("{ANSI_CLEAR}Hallo, mate!");
|
||||
async fn run() -> Result<()> {
|
||||
print!("{ANSI_CLEAR}");
|
||||
let tasks = Tasks::try_new()?;
|
||||
let gitlab_user = tasks.gitlab.me().await?;
|
||||
info!("{gitlab_user:#?}");
|
||||
let jira_user = tasks.jira.me().await?;
|
||||
info!("{jira_user:#?}");
|
||||
let issues = tasks.jira.assigned_open_issues().await?;
|
||||
info!("{issues:#?}");
|
||||
info!("{}", issues.len());
|
||||
Ok(())
|
||||
}
|
||||
|
|
3
src/result.rs
Normal file
3
src/result.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
use anyhow::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
28
src/tasks.rs
28
src/tasks.rs
|
@ -1,14 +1,32 @@
|
|||
use std::env;
|
||||
use std::{env, process::Command};
|
||||
|
||||
use crate::gitlab::GitLab;
|
||||
use crate::{gitlab::GitLab, jira::Jira, result::Result};
|
||||
|
||||
pub struct Tasks {
|
||||
pub gitlab: GitLab,
|
||||
pub jira: Jira,
|
||||
}
|
||||
|
||||
impl Tasks {
|
||||
pub fn try_new() -> Result<Self, anyhow::Error> {
|
||||
let gitlab = GitLab::try_new("https://git.hq.bill.com/api/v4", &env::var("GITLAB_TOKEN")?)?;
|
||||
Ok(Self { gitlab })
|
||||
pub fn try_new() -> Result<Self> {
|
||||
let gl_token = env::var("GITLAB_TOKEN").or_else(|_| -> Result<String> {
|
||||
let output = Command::new("pass")
|
||||
.arg("client/divvy/gitlab-glpat")
|
||||
.output()?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
})?;
|
||||
let gitlab = GitLab::try_new("https://git.hq.bill.com/api/v4", &gl_token)?;
|
||||
|
||||
let jira_token = env::var("JIRA_TOKEN").or_else(|_| -> Result<String> {
|
||||
let output = Command::new("pass")
|
||||
.arg("client/divvy/jira-api-token-with-email")
|
||||
.output()?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
})?;
|
||||
let jira = Jira::try_new("https://billcom.atlassian.net", &jira_token)?;
|
||||
|
||||
Ok(Self { gitlab, jira })
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue