pub mod pull_requests; pub mod release_notes; use app::constants::{ APP_USER_AGENT, FLIGHTCORE_REPO_NAME, NORTHSTAR_RELEASE_REPO_NAME, SECTION_ORDER, }; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, TS)] #[ts(export)] pub struct Tag { name: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[ts(export)] pub enum Project { FlightCore, Northstar, } /// Wrapper type needed for frontend #[derive(Serialize, Deserialize, Debug, Clone, TS)] #[ts(export)] pub struct TagWrapper { label: String, value: Tag, } #[derive(Debug, Deserialize)] struct CommitInfo { sha: String, commit: Commit, author: Option<CommitAuthor>, } #[derive(Debug, Deserialize)] struct Commit { message: String, } #[derive(Debug, Deserialize)] struct CommitAuthor { login: String, } #[derive(Debug, Deserialize)] struct Comparison { commits: Vec<CommitInfo>, } /// Get a list of tags on the FlightCore repo #[tauri::command] pub fn get_list_of_tags(project: Project) -> Result<Vec<TagWrapper>, String> { // Set the repository name. // Create a `reqwest` client with a user agent. let client = reqwest::blocking::Client::builder() .user_agent(APP_USER_AGENT) .build() .unwrap(); // Switch repo to fetch from based on project let repo_name = match project { Project::FlightCore => FLIGHTCORE_REPO_NAME, Project::Northstar => NORTHSTAR_RELEASE_REPO_NAME, }; // Fetch the list of tags for the repository as a `Vec<Tag>`. let tags_url = format!("https://api.github.com/repos/{}/tags", repo_name); let tags: Vec<Tag> = client.get(tags_url).send().unwrap().json().unwrap(); // Map each `Tag` element to a `TagWrapper` element with the desired label and `Tag` value. let tag_wrappers: Vec<TagWrapper> = tags .into_iter() .map(|tag| TagWrapper { label: tag.name.clone(), value: tag, }) .collect(); Ok(tag_wrappers) } /// Use GitHub API to compare two tags of the same repo against each other and get the resulting changes #[tauri::command] pub fn compare_tags(project: Project, first_tag: Tag, second_tag: Tag) -> Result<String, String> { match project { Project::FlightCore => compare_tags_flightcore(first_tag, second_tag), Project::Northstar => compare_tags_northstar(first_tag, second_tag), } } pub fn compare_tags_flightcore(first_tag: Tag, second_tag: Tag) -> Result<String, String> { // Fetch the list of commits between the two tags. // Create a `reqwest` client with a user agent. let client = reqwest::blocking::Client::builder() .user_agent(APP_USER_AGENT) .build() .unwrap(); let repo = "R2NorthstarTools/FlightCore"; let mut full_patch_notes = "".to_string(); let mut patch_notes: Vec<String> = [].to_vec(); println!("{}", repo); // let repo = "R2Northstar/NorthstarLauncher"; let comparison_url = format!( "https://api.github.com/repos/{}/compare/{}...{}", repo, first_tag.name, second_tag.name ); let comparison: Comparison = client.get(comparison_url).send().unwrap().json().unwrap(); let commits = comparison.commits; // Display the list of commits. println!( "Commits between {} and {}:", first_tag.name, second_tag.name ); // Iterate over all commits in the diff for commit in commits { println!( " * {} : {}", commit.sha, commit.commit.message.split('\n').next().unwrap() ); patch_notes.push( commit .commit .message .split('\n') .next() .unwrap() .to_string(), ); } full_patch_notes += &generate_flightcore_release_notes(patch_notes); Ok(full_patch_notes.to_string()) } /// Generate release notes in the format used for FlightCore fn generate_flightcore_release_notes(commits: Vec<String>) -> String { let grouped_commits = group_commits_by_type(commits); let mut release_notes = String::new(); // Go over commit types and generate notes for commit_type in SECTION_ORDER { if let Some(commit_list) = grouped_commits.get(commit_type) { if !commit_list.is_empty() { let section_title = match commit_type { "feat" => "**Features:**", "fix" => "**Bug Fixes:**", "docs" => "**Documentation:**", "style" => "**Styles:**", "refactor" => "**Code Refactoring:**", "build" => "**Build:**", "test" => "**Tests:**", "chore" => "**Chores:**", "i18n" => "**Translations:**", _ => "**Other:**", }; release_notes.push_str(&format!("{}\n", section_title)); for commit_message in commit_list { release_notes.push_str(&format!("- {}\n", commit_message)); } release_notes.push('\n'); } } } release_notes } /// Group semantic commit messages by type /// Commmit messages that are not formatted accordingly are marked as "other" fn group_commits_by_type(commits: Vec<String>) -> HashMap<String, Vec<String>> { let mut grouped_commits: HashMap<String, Vec<String>> = HashMap::new(); let mut other_commits: Vec<String> = vec![]; for commit in commits { let commit_parts: Vec<&str> = commit.splitn(2, ':').collect(); if commit_parts.len() == 2 { let commit_type = commit_parts[0].to_lowercase(); let commit_description = commit_parts[1].trim().to_string(); // Check if known commit type if SECTION_ORDER.contains(&commit_type.as_str()) { let commit_list = grouped_commits.entry(commit_type.to_string()).or_default(); commit_list.push(commit_description); } else { // otherwise add to list of "other" other_commits.push(commit.to_string()); } } else { other_commits.push(commit.to_string()); } } grouped_commits.insert("other".to_string(), other_commits); grouped_commits } /// Compares two tags on Northstar repo and generates release notes over the diff in tags /// over the 3 major repos (Northstar, NorthstarLauncher, NorthstarMods) pub fn compare_tags_northstar(first_tag: Tag, second_tag: Tag) -> Result<String, String> { // Fetch the list of commits between the two tags. // Create a `reqwest` client with a user agent. let client = reqwest::blocking::Client::builder() .user_agent(APP_USER_AGENT) .build() .unwrap(); let repos = [ "R2Northstar/Northstar", "R2Northstar/NorthstarLauncher", "R2Northstar/NorthstarMods", ]; let mut full_patch_notes = "".to_string(); let mut authors_set = std::collections::HashSet::new(); for repo in repos { full_patch_notes += &format!("{}\n\n", repo); let mut patch_notes: Vec<String> = [].to_vec(); println!("{}", repo); // let repo = "R2Northstar/NorthstarLauncher"; let comparison_url = format!( "https://api.github.com/repos/{}/compare/{}...{}", repo, first_tag.name, second_tag.name ); log::info!("Compare URL: {}", comparison_url.clone()); let comparison: Comparison = client.get(&comparison_url).send().unwrap().json().unwrap(); let commits = comparison.commits; // Display the list of commits. println!( "Commits between {} and {}:", first_tag.name, second_tag.name ); // for commit in commits { println!( " * {} : {}", commit.sha, turn_pr_number_into_link(commit.commit.message.split('\n').next().unwrap(), repo) ); patch_notes.push(turn_pr_number_into_link( commit.commit.message.split('\n').next().unwrap(), repo, )); // Store authors in set if commit.author.is_some() { authors_set.insert(commit.author.unwrap().login); } } full_patch_notes += &patch_notes.join("\n"); full_patch_notes += "\n\n\n"; } // Convert the set to a sorted vector. let mut sorted_vec: Vec<String> = authors_set.into_iter().collect(); sorted_vec.sort(); // Define a string to prepend to each element. let prefix = "@"; // Create a new list with the prefix prepended to each element. let prefixed_list: Vec<String> = sorted_vec.iter().map(|s| prefix.to_owned() + s).collect(); full_patch_notes += "**Contributors:**\n"; full_patch_notes += &prefixed_list.join(" "); Ok(full_patch_notes.to_string()) } /// Takes the commit title and repo slug and formats it as /// `[commit title(SHORTENED_REPO#NUMBER)](LINK)` fn turn_pr_number_into_link(input: &str, repo: &str) -> String { // Extract `Mods/Launcher` from repo title let last_line = repo .split('/') .rev() .next() .unwrap() .trim_start_matches("Northstar"); // Extract PR number let re = Regex::new(r"#(\d+)").unwrap(); // Generate pull request link let pull_link = format!("https://github.com/{}/pull/", repo); re.replace_all(input, format!("[{}#$1]({}$1)", last_line, pull_link)) .to_string() }