diff options
-rw-r--r-- | src-tauri/bindings/Tag.ts | 3 | ||||
-rw-r--r-- | src-tauri/bindings/TagWrapper.ts | 4 | ||||
-rw-r--r-- | src-tauri/src/constants.rs | 8 | ||||
-rw-r--r-- | src-tauri/src/github/mod.rs | 176 | ||||
-rw-r--r-- | src-tauri/src/main.rs | 3 | ||||
-rw-r--r-- | src-vue/src/views/DeveloperView.vue | 95 |
6 files changed, 289 insertions, 0 deletions
diff --git a/src-tauri/bindings/Tag.ts b/src-tauri/bindings/Tag.ts new file mode 100644 index 00000000..adbbff33 --- /dev/null +++ b/src-tauri/bindings/Tag.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface Tag { name: string, }
\ No newline at end of file diff --git a/src-tauri/bindings/TagWrapper.ts b/src-tauri/bindings/TagWrapper.ts new file mode 100644 index 00000000..f9f56a51 --- /dev/null +++ b/src-tauri/bindings/TagWrapper.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Tag } from "./Tag"; + +export interface TagWrapper { label: string, value: Tag, }
\ No newline at end of file diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 7b13206a..0f4e9ab4 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -29,6 +29,11 @@ pub const BLACKLISTED_MODS: [&str; 3] = [ // Titanfall2 game IDs on Origin/EA-App pub const TITANFALL2_ORIGIN_IDS: [&str; 2] = ["Origin.OFR.50.0001452", "Origin.OFR.50.0001456"]; +// Order in which the sections for release notes should be displayed +pub const SECTION_ORDER: [&str; 9] = [ + "feat", "fix", "docs", "style", "refactor", "build", "test", "chore", "other", +]; + // GitHub API endpoints for launcher/mods PRs pub const PULLS_API_ENDPOINT_LAUNCHER: &str = "https://api.github.com/repos/R2Northstar/NorthstarLauncher/pulls"; @@ -37,3 +42,6 @@ pub const PULLS_API_ENDPOINT_MODS: &str = // Statistics (players and servers counts) refresh delay pub const REFRESH_DELAY: Duration = Duration::from_secs(5 * 60); + +// Flightcore repo name and org name on GitHub +pub const FLIGHTCORE_REPO_NAME: &str = "R2NorthstarTools/FlightCore"; diff --git a/src-tauri/src/github/mod.rs b/src-tauri/src/github/mod.rs index 942f0db2..87ea629c 100644 --- a/src-tauri/src/github/mod.rs +++ b/src-tauri/src/github/mod.rs @@ -1,2 +1,178 @@ pub mod pull_requests; pub mod release_notes; + +use app::constants::{APP_USER_AGENT, FLIGHTCORE_REPO_NAME, SECTION_ORDER}; +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, +} + +/// 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, +} + +#[derive(Debug, Deserialize)] +struct Commit { + message: 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() -> 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(); + + // Fetch the list of tags for the repository as a `Vec<Tag>`. + let tags_url = format!("https://api.github.com/repos/{}/tags", FLIGHTCORE_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(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(format!( + "{}", + commit.commit.message.split('\n').next().unwrap() + )); + } + + 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:**", + _ => "**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_str("\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 +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index dfceece7..f4712cd4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -21,6 +21,7 @@ use github::pull_requests::{ use github::release_notes::{ check_is_flightcore_outdated, get_newest_flightcore_version, get_northstar_release_notes, }; +use github::{compare_tags, get_list_of_tags}; mod repair_and_verify; use repair_and_verify::{ @@ -137,6 +138,8 @@ fn main() { delete_thunderstore_mod, open_repair_window, query_thunderstore_packages_api, + get_list_of_tags, + compare_tags, get_pull_requests_wrapper, apply_launcher_pr, apply_mods_pr, diff --git a/src-vue/src/views/DeveloperView.vue b/src-vue/src/views/DeveloperView.vue index 52117b8d..bca473fb 100644 --- a/src-vue/src/views/DeveloperView.vue +++ b/src-vue/src/views/DeveloperView.vue @@ -5,6 +5,38 @@ This page is designed for developers. Some of the buttons here can break your Northstar install if you do not know what you're doing! </el-alert> + <el-button type="primary" @click="getTags"> + Get tags + </el-button> + + <el-select v-model="firstTag" class="m-2" placeholder="First tag"> + <el-option + v-for="item in ns_release_tags" + :key="item.value" + :label="item.label" + :value="item" + /> + </el-select> + <el-select v-model="secondTag" class="m-2" placeholder="Second tag"> + <el-option + v-for="item in ns_release_tags" + :key="item.value" + :label="item.label" + :value="item" + /> + </el-select> + + <el-button type="primary" @click="compareTags"> + Compare Tags + </el-button> + + <el-input + v-model="release_notes_text" + type="textarea" + :rows="5" + placeholder="Output" + /> + <h3>Basic:</h3> <el-button type="primary" @click="disableDevMode"> @@ -53,6 +85,7 @@ import { defineComponent } from "vue"; import { invoke } from "@tauri-apps/api"; import { ElNotification } from "element-plus"; import { GameInstall } from "../utils/GameInstall"; +import { TagWrapper } from "../../../src-tauri/bindings/TagWrapper"; import PullRequestsSelector from "../components/PullRequestsSelector.vue"; export default defineComponent({ @@ -63,8 +96,30 @@ export default defineComponent({ data() { return { mod_to_install_field_string : "", + release_notes_text : "", + first_tag: { label: '', value: {name: ''} }, + second_tag: { label: '', value: {name: ''} }, + ns_release_tags: [] as TagWrapper[], } }, + computed: { + firstTag: { + get(): TagWrapper { + return this.first_tag; + }, + set(value: TagWrapper) { + this.first_tag = value; + } + }, + secondTag: { + get(): TagWrapper { + return this.second_tag; + }, + set(value: TagWrapper) { + this.second_tag = value; + } + }, + }, methods: { disableDevMode() { this.$store.commit('toggleDeveloperMode'); @@ -152,6 +207,46 @@ export default defineComponent({ }); }); }, + async getTags() { + await invoke<TagWrapper[]>("get_list_of_tags") + .then((message) => { + this.ns_release_tags = message; + ElNotification({ + title: "Done", + message: "Fetched tags", + type: 'success', + position: 'bottom-right' + }); + }) + .catch((error) => { + ElNotification({ + title: 'Error', + message: error, + type: 'error', + position: 'bottom-right' + }); + }); + }, + async compareTags() { + await invoke<string>("compare_tags", {firstTag: this.firstTag.value, secondTag: this.secondTag.value}) + .then((message) => { + this.release_notes_text = message; + ElNotification({ + title: "Done", + message: "Generated release notes", + type: 'success', + position: 'bottom-right' + }); + }) + .catch((error) => { + ElNotification({ + title: 'Error', + message: error, + type: 'error', + position: 'bottom-right' + }); + }); + }, } }); </script> |