diff --git a/devprofiler/src/bitbucket/auth.rs b/devprofiler/src/bitbucket/auth.rs index 8325761..28db007 100644 --- a/devprofiler/src/bitbucket/auth.rs +++ b/devprofiler/src/bitbucket/auth.rs @@ -62,7 +62,7 @@ pub async fn refresh_git_auth(clone_url: &str, directory: &str) -> String{ return access_token; } -async fn update_access_token(auth_info: &AuthInfo) -> Option { +pub async fn update_access_token(auth_info: &AuthInfo) -> Option { let now = SystemTime::now(); let now_secs = now.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs(); diff --git a/devprofiler/src/bitbucket/comment.rs b/devprofiler/src/bitbucket/comment.rs new file mode 100644 index 0000000..1b05596 --- /dev/null +++ b/devprofiler/src/bitbucket/comment.rs @@ -0,0 +1,84 @@ +use std::{env, collections::HashMap}; + +use reqwest::{Response, header::{HeaderMap, HeaderValue}}; +use serde::Serialize; +use serde_json::Value; + +use crate::db::auth::auth_info; + +use super::{config::bitbucket_base_url, auth::update_access_token}; + +#[derive(Serialize)] +struct Comment { + content: Content, +} + +#[derive(Serialize)] +struct Content { + raw: String, +} +pub async fn add_comment(workspace_name: &str, repo_name: &str, review_id: &str, comment_text: &str) { + let url = format!("{}/repositories/{workspace_name}/{repo_name}/pullrequests/{review_id}/comments", bitbucket_base_url()); + println!("comment url = {}", &url); + let auth_info = auth_info(); + let mut access_token = auth_info.access_token().clone(); + let new_auth_opt = update_access_token(&auth_info).await; + if new_auth_opt.is_some() { + let new_auth = new_auth_opt.expect("new_auth_opt is empty"); + access_token = new_auth.access_token().clone(); + } + let comment_payload = Comment { + content: Content { + raw: comment_text.to_string(), + }, + }; + let client = reqwest::Client::new(); + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( reqwest::header::AUTHORIZATION, + format!("Bearer {}", access_token).parse().expect("Invalid auth header"), ); + headers.insert("Accept", + "application/json".parse().expect("Invalid Accept header")); + let response_res = client.post(&url). + headers(headers).json(&comment_payload).send().await; + if response_res.is_err() { + eprintln!("Error in post request for adding comment - {:?}", response_res.as_ref().expect_err("response has no error")); + } + let response = response_res.expect("Error in getting response"); + if response.status().is_success() { + eprintln!("Failed to call API {}, status: {}", url, response.status()); + } + let response_json = response.json::().await; + println!("response from comment api = {:?}", &response_json); +} + +pub async fn add_reviewers(repo_owner: &str, repo_name: &str, user_id: &str) { + let url = format!("{}/repositories/{repo_owner}/{repo_name}/default-reviewers/{user_id}", bitbucket_base_url()); + println!("auto assign url = {}", &url); + let auth_info = auth_info(); + let mut access_token = auth_info.access_token().clone(); + let new_auth_opt = update_access_token(&auth_info).await; + if new_auth_opt.is_some() { + let new_auth = new_auth_opt.expect("new_auth_opt is empty"); + access_token = new_auth.access_token().clone(); + } + let client = reqwest::Client::new(); + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( reqwest::header::AUTHORIZATION, + format!("Bearer {}", access_token).parse().expect("Invalid auth header"), ); + headers.insert("Accept", + "application/json".parse().expect("Invalid Accept header")); + let response_res = client + .put(&url) + .bearer_auth(access_token) // Use Bearer authentication with your personal access token + .header("Accept", "application/json") + .send().await; + if response_res.is_err() { + eprintln!("Error in post request for auto assign - {:?}", response_res.as_ref().expect_err("response has no error")); + } + let response = response_res.expect("Error in getting response"); + if response.status().is_success() { + eprintln!("Failed to call API {}, status: {}", url, response.status()); + } + let response_json = response.json::().await; + println!("response from auto assign api = {:?}", &response_json); +} \ No newline at end of file diff --git a/devprofiler/src/bitbucket/config.rs b/devprofiler/src/bitbucket/config.rs index 7a49b98..8c48751 100644 --- a/devprofiler/src/bitbucket/config.rs +++ b/devprofiler/src/bitbucket/config.rs @@ -9,6 +9,7 @@ pub fn bitbucket_base_url() -> String { pub async fn get_api(url: &str, access_token: &str, params: Option> ) -> Vec { let response_opt = call_get_api(url, access_token, ¶ms).await; + println!("response of get_api = {:?}", &response_opt); let (mut response_values, next_url) = deserialize_response(response_opt).await; if next_url.is_some() { let mut page_values = get_all_pages(next_url, access_token, ¶ms).await; @@ -17,7 +18,7 @@ pub async fn get_api(url: &str, access_token: &str, params: Option> ) -> Option{ +pub async fn call_get_api(url: &str, access_token: &str, params: &Option> ) -> Option{ println!("GET api url = {}", url); let client = reqwest::Client::new(); let mut headers = reqwest::header::HeaderMap::new(); diff --git a/devprofiler/src/bitbucket/mod.rs b/devprofiler/src/bitbucket/mod.rs index f0db7d0..6772197 100644 --- a/devprofiler/src/bitbucket/mod.rs +++ b/devprofiler/src/bitbucket/mod.rs @@ -3,4 +3,5 @@ pub mod workspace; pub mod repo; mod config; pub mod webhook; -pub mod user; \ No newline at end of file +pub mod user; +pub mod comment; \ No newline at end of file diff --git a/devprofiler/src/bitbucket/user.rs b/devprofiler/src/bitbucket/user.rs index 6a9d8f1..5381c4a 100644 --- a/devprofiler/src/bitbucket/user.rs +++ b/devprofiler/src/bitbucket/user.rs @@ -1,8 +1,10 @@ +use chrono::{DateTime, Utc, FixedOffset}; use crate::db::auth::auth_info; -use crate::db::user::save_user_to_db; +use crate::db::user::{save_user_to_db, user_from_db}; use crate::utils::auth::AuthInfo; +use crate::utils::lineitem::LineItem; use crate::utils::user::{User, Provider, ProviderEnum}; -use super::config::{bitbucket_base_url, get_api}; +use super::config::{bitbucket_base_url, get_api, call_get_api}; pub async fn get_and_save_workspace_users(workspace_id: &str, access_token: &str) { let base_url = bitbucket_base_url(); @@ -21,12 +23,36 @@ pub async fn get_and_save_workspace_users(workspace_id: &str, access_token: &str } } -pub async fn get_commit_bb(commit: &str, repo_name: &str, repo_owner: &str) { +pub async fn get_commit_bb(commit: &str, repo_name: &str, repo_owner: &str) -> LineItem{ let base_url = bitbucket_base_url(); let commits_url = format!("{}/repositories/{repo_owner}/{repo_name}/commit/{commit}", &base_url); println!("commits url = {}", &commits_url); let authinfo: AuthInfo = auth_info(); let access_token = authinfo.access_token(); - let response_json = get_api(&commits_url, access_token, None).await; - println!("response json for commits url = {:?}", &response_json); + let response = call_get_api(&commits_url, access_token, &None).await; + let response_json = response.expect("No response").json::().await.expect("Error in deserializing json"); + let timestamp_str = &response_json["date"].to_string().replace('"', ""); + println!("timestamp_str = {}", timestamp_str); + // Explicitly specify the format + let datetime: DateTime = DateTime::parse_from_rfc3339(×tamp_str) + .expect("Failed to parse timestamp"); + + // Convert to Utc + let datetime_utc = datetime.with_timezone(&Utc); + + let unix_timestamp = datetime_utc.timestamp(); + let unix_timestamp_str = unix_timestamp.to_string(); + let author_id = response_json["author"]["user"]["uuid"].to_string().replace('"', ""); + let author_name = response_json["author"]["user"]["display_name"].to_string().replace('"', ""); + let user_opt = user_from_db( + &ProviderEnum::Bitbucket.to_string(), + repo_owner, &author_id); + if user_opt.is_none() { + let user = User::new( + Provider::new(author_id.clone(), + ProviderEnum::Bitbucket), + author_name, repo_owner.to_string(), None); + save_user_to_db(&user); + } + return LineItem::new(author_id, unix_timestamp_str); } \ No newline at end of file diff --git a/devprofiler/src/core/coverage.rs b/devprofiler/src/core/coverage.rs index 27807c2..b9d9639 100644 --- a/devprofiler/src/core/coverage.rs +++ b/devprofiler/src/core/coverage.rs @@ -1,7 +1,82 @@ -use crate::utils::hunk::HunkMap; +use std::collections::HashMap; + +use crate::{utils::hunk::{HunkMap, PrHunkItem}, db::user::user_from_db, bitbucket::comment::{add_comment, add_reviewers}}; pub async fn process_coverage(hunkmap: &HunkMap) { - // get current reviewers - // calculate coverage - -} \ No newline at end of file + for prhunk in hunkmap.prhunkvec() { + // calculate number of hunks for each userid + let coverage_map = calculate_coverage(&hunkmap.repo_owner(), prhunk); + if !coverage_map.is_empty() { + // get user for each user id + // create comment text + let comment = comment_text(coverage_map); + // add comment + add_comment(&hunkmap.repo_owner(), + &hunkmap.repo_name(), + prhunk.pr_number(), + &comment).await; + // add reviewers + for blame in prhunk.blamevec() { + let author_id = blame.author(); + add_reviewers(&hunkmap.repo_owner(), + &hunkmap.repo_name(), + &author_id).await; + } + // TODO - implement settings + } + } +} + +fn calculate_coverage(repo_owner: &str, prhunk: &PrHunkItem) -> HashMap{ + let mut coverage_map = HashMap::::new(); + let mut coverage_floatmap = HashMap::::new(); + let mut total = 0.0; + for blame in prhunk.blamevec() { + let author_id = blame.author().to_owned(); + let num_lines: f32 = blame.line_end().parse::().expect("lines_end invalid float") + - blame.line_start().parse::().expect("lines_end invalid float") + + 1.0; + total += num_lines; + if coverage_floatmap.contains_key(&author_id) { + let coverage = coverage_floatmap.get(&author_id).expect("unable to find coverage for author") + + num_lines; + coverage_floatmap.insert(author_id, coverage); + } + else { + coverage_floatmap.insert(author_id, num_lines); + } + } + if total <= 0.0 { + return coverage_map; + } + for (key, value) in coverage_floatmap.iter_mut() { + *value = *value / total * 100.0; + let formatted_value = format!("{:.2}", *value); + let user = user_from_db("bitbucket", repo_owner, key); + if user.is_none() { + eprintln!("No user name found for {}", key); + coverage_map.insert(key.to_string(), formatted_value); + continue; + } + let user_val = user.expect("user is empty"); + let coverage_key = user_val.name(); + coverage_map.insert(coverage_key.to_string(), formatted_value); + } + return coverage_map; +} + +fn comment_text(coverage_map: HashMap) -> String { + let mut comment = "Relevant users for this PR:\n\n".to_string(); // Added two newlines + comment += "| Contributor Name/Alias | Code Coverage |\n"; // Added a newline at the end + comment += "| -------------- | --------------- |\n"; // Added a newline at the end + + for (key, value) in coverage_map.iter() { + comment += &format!("| {} | {}% |\n", key, value); // Added a newline at the end + } + + comment += "\n\n"; + comment += "Code coverage is calculated based on the git blame information of the PR. To know more, hit us up at contact@vibinex.com.\n\n"; // Added two newlines + comment += "To change comment and auto-assign settings, go to [your Vibinex settings page.](https://vibinex.com/settings)\n"; // Added a newline at the end + + return comment; +} diff --git a/devprofiler/src/db/user.rs b/devprofiler/src/db/user.rs index 4c8cf04..ba21956 100644 --- a/devprofiler/src/db/user.rs +++ b/devprofiler/src/db/user.rs @@ -16,3 +16,17 @@ pub fn save_user_to_db(user: &User) { // Insert JSON into sled DB db.insert(IVec::from(user_key.as_bytes()), json).expect("Failed to upsert user into sled DB"); } + +pub fn user_from_db(repo_provider: &str, workspace: &str, user_id: &str, ) -> Option { + let db = get_db(); + let user_key = format!("{}/{}/{}", + repo_provider, workspace, user_id); + let user_opt = db.get(IVec::from(user_key.as_bytes())).expect("Unable to get repo from db"); + if user_opt.is_none() { + return None; + } + let user_ivec = user_opt.expect("Empty value"); + let user: User = serde_json::from_slice::(&user_ivec).unwrap(); + println!("user from db = {:?}", &user); + return Some(user); +} \ No newline at end of file diff --git a/devprofiler/src/utils/gitops.rs b/devprofiler/src/utils/gitops.rs index c57c2e2..d535892 100644 --- a/devprofiler/src/utils/gitops.rs +++ b/devprofiler/src/utils/gitops.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::error::Error; use std::process::Command; use std::str; use serde::Deserialize; @@ -11,6 +10,7 @@ use crate::bitbucket::user::get_commit_bb; use super::hunk::BlameItem; use super::review::Review; +use super::lineitem::LineItem; #[derive(Debug, Serialize, Default, Deserialize)] pub struct StatItem { @@ -268,13 +268,13 @@ pub async fn generate_blame(review: &Review, linemap: &HashMap &String { - &self.author - } - - fn timestamp(&self) -> &String { - &self.timestamp - } -} - async fn process_blamelines(blamelines: &Vec<&str>, linenum: usize, repo_name: &str, repo_owner: &str) -> HashMap { let mut linemap = HashMap::::new(); @@ -331,32 +316,9 @@ async fn process_blamelines(blamelines: &Vec<&str>, linenum: usize, let wordvec: Vec<&str> = ln.split(" ").collect(); let commit = wordvec[0]; let lineitem = get_commit_bb(commit, repo_name, repo_owner).await; - let mut author = wordvec[1]; - let mut timestamp = wordvec[2]; - let mut idx = 1; - if author == "" { - while idx < wordvec.len() && wordvec[idx] == "" { - idx = idx + 1; - } - if idx < wordvec.len() { - author = wordvec[idx]; - } - } - let authorstr = author.replace("(", "") - .replace("<", "") - .replace(">", ""); - if timestamp == "" { - idx = idx + 1; - while idx < wordvec.len() && wordvec[idx] == "" { - idx = idx + 1; - } - if idx < wordvec.len() { - timestamp = wordvec[idx]; - } - } linemap.insert( linenum + lnum, - LineItem { author: authorstr.to_string(), timestamp: timestamp.to_string() } + lineitem ); } return linemap; diff --git a/devprofiler/src/utils/lineitem.rs b/devprofiler/src/utils/lineitem.rs new file mode 100644 index 0000000..52be0fc --- /dev/null +++ b/devprofiler/src/utils/lineitem.rs @@ -0,0 +1,22 @@ +#[derive(Debug, Clone)] +pub struct LineItem { + author_id: String, + timestamp: String, +} + +impl LineItem { + pub fn new(author_id: String, timestamp: String) -> Self { + Self { + author_id, + timestamp + } + } + + pub fn author_id(&self) -> &String { + &self.author_id + } + + pub fn timestamp(&self) -> &String { + &self.timestamp + } +} \ No newline at end of file diff --git a/devprofiler/src/utils/mod.rs b/devprofiler/src/utils/mod.rs index 3532717..0afbdf8 100644 --- a/devprofiler/src/utils/mod.rs +++ b/devprofiler/src/utils/mod.rs @@ -5,4 +5,5 @@ pub mod webhook; pub mod hunk; pub mod review; pub mod gitops; -pub mod user; \ No newline at end of file +pub mod user; +pub mod lineitem; \ No newline at end of file