From 4ee01031f629f3291dd10c0f7a2658d782c30ab2 Mon Sep 17 00:00:00 2001 From: estib Date: Thu, 17 Oct 2024 16:23:56 +0200 Subject: [PATCH] gitbulter-forge crate Create a centralized crate for forge actions that is provider agnostic. Move the logic behind fetching the PR templates to it. --- Cargo.lock | 11 +++ Cargo.toml | 1 + crates/gitbutler-forge/Cargo.toml | 11 +++ crates/gitbutler-forge/src/forge.rs | 11 +++ crates/gitbutler-forge/src/lib.rs | 2 + crates/gitbutler-forge/src/review.rs | 103 ++++++++++++++++++++++++ crates/gitbutler-project/Cargo.toml | 1 + crates/gitbutler-project/src/project.rs | 19 ++++- crates/gitbutler-tauri/Cargo.toml | 1 + crates/gitbutler-tauri/src/forge.rs | 24 ++++++ crates/gitbutler-tauri/src/github.rs | 27 +------ crates/gitbutler-tauri/src/lib.rs | 1 + crates/gitbutler-tauri/src/main.rs | 9 ++- crates/gitbutler-tauri/src/projects.rs | 24 +++++- 14 files changed, 211 insertions(+), 34 deletions(-) create mode 100644 crates/gitbutler-forge/Cargo.toml create mode 100644 crates/gitbutler-forge/src/forge.rs create mode 100644 crates/gitbutler-forge/src/lib.rs create mode 100644 crates/gitbutler-forge/src/review.rs create mode 100644 crates/gitbutler-tauri/src/forge.rs diff --git a/Cargo.lock b/Cargo.lock index 50198d621..bb1de4974 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2339,6 +2339,15 @@ dependencies = [ "zip", ] +[[package]] +name = "gitbutler-forge" +version = "0.0.0" +dependencies = [ + "anyhow", + "gitbutler-fs", + "serde", +] + [[package]] name = "gitbutler-fs" version = "0.0.0" @@ -2464,6 +2473,7 @@ dependencies = [ "fslock", "git2", "gitbutler-error", + "gitbutler-forge", "gitbutler-id", "gitbutler-serde", "gitbutler-storage", @@ -2648,6 +2658,7 @@ dependencies = [ "gitbutler-edit-mode", "gitbutler-error", "gitbutler-feedback", + "gitbutler-forge", "gitbutler-fs", "gitbutler-id", "gitbutler-operating-modes", diff --git a/Cargo.toml b/Cargo.toml index ef5ab5a24..2347e8844 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ gitbutler-oxidize = { path = "crates/gitbutler-oxidize" } gitbutler-stack-api = { path = "crates/gitbutler-stack-api" } gitbutler-stack = { path = "crates/gitbutler-stack" } gitbutler-patch-reference = { path = "crates/gitbutler-patch-reference" } +gitbutler-forge = { path = "crates/gitbutler-forge" } [profile.release] codegen-units = 1 # Compile crates one after another so the compiler can optimize better diff --git a/crates/gitbutler-forge/Cargo.toml b/crates/gitbutler-forge/Cargo.toml new file mode 100644 index 000000000..a967d75ad --- /dev/null +++ b/crates/gitbutler-forge/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "gitbutler-forge" +version = "0.0.0" +edition = "2021" +authors = ["GitButler "] +publish = false + +[dependencies] +serde = { workspace = true, features = ["std"] } +anyhow = "1.0.86" +gitbutler-fs.workspace = true \ No newline at end of file diff --git a/crates/gitbutler-forge/src/forge.rs b/crates/gitbutler-forge/src/forge.rs new file mode 100644 index 000000000..ca5abf8cf --- /dev/null +++ b/crates/gitbutler-forge/src/forge.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(tag = "type", rename_all = "lowercase")] +/// Supported git forge types +pub enum ForgeType { + GitHub, + GitLab, + Bitbucket, + Azure, +} diff --git a/crates/gitbutler-forge/src/lib.rs b/crates/gitbutler-forge/src/lib.rs new file mode 100644 index 000000000..5a415af58 --- /dev/null +++ b/crates/gitbutler-forge/src/lib.rs @@ -0,0 +1,2 @@ +pub mod forge; +pub mod review; diff --git a/crates/gitbutler-forge/src/review.rs b/crates/gitbutler-forge/src/review.rs new file mode 100644 index 000000000..afbddbd35 --- /dev/null +++ b/crates/gitbutler-forge/src/review.rs @@ -0,0 +1,103 @@ +use std::path; + +use gitbutler_fs::list_files; + +use crate::forge::ForgeType; + +/// Get a list of available review template paths for a project +/// +/// The paths are relative to the root path +pub fn available_review_templates(root_path: &path::Path, forge_type: &ForgeType) -> Vec { + let (is_review_template, get_root) = match forge_type { + ForgeType::GitHub => ( + is_review_template_github as fn(&str) -> bool, + get_github_directory_path as fn(&path::Path) -> path::PathBuf, + ), + ForgeType::GitLab => ( + is_review_template_gitlab as fn(&str) -> bool, + get_gitlab_directory_path as fn(&path::Path) -> path::PathBuf, + ), + ForgeType::Bitbucket => ( + is_review_template_bitbucket as fn(&str) -> bool, + get_bitbucket_directory_path as fn(&path::Path) -> path::PathBuf, + ), + ForgeType::Azure => ( + is_review_template_azure as fn(&str) -> bool, + get_azure_directory_path as fn(&path::Path) -> path::PathBuf, + ), + }; + + let forge_root_path = get_root(root_path); + let forge_root_path = forge_root_path.as_path(); + + let walked_paths = list_files(forge_root_path, &[forge_root_path]).unwrap_or_default(); + + let mut available_paths = Vec::new(); + for entry in walked_paths { + let path_entry = entry.as_path(); + let path_str = path_entry.to_string_lossy(); + + if is_review_template(&path_str) { + if let Ok(template_path) = forge_root_path.join(path_entry).strip_prefix(root_path) { + available_paths.push(template_path.to_string_lossy().to_string()); + } + } + } + + available_paths +} + +fn get_github_directory_path(root_path: &path::Path) -> path::PathBuf { + let mut path = root_path.to_path_buf(); + path.push(".github"); + path +} + +fn is_review_template_github(path_str: &str) -> bool { + path_str == "PULL_REQUEST_TEMPLATE.md" + || path_str == "pull_request_template.md" + || path_str.contains("PULL_REQUEST_TEMPLATE/") +} + +fn get_gitlab_directory_path(root_path: &path::Path) -> path::PathBuf { + // TODO: implement + root_path.to_path_buf() +} + +fn is_review_template_gitlab(_path_str: &str) -> bool { + // TODO: implement + false +} + +fn get_bitbucket_directory_path(root_path: &path::Path) -> path::PathBuf { + // TODO: implement + root_path.to_path_buf() +} + +fn is_review_template_bitbucket(_path_str: &str) -> bool { + // TODO: implement + false +} + +fn get_azure_directory_path(root_path: &path::Path) -> path::PathBuf { + // TODO: implement + root_path.to_path_buf() +} + +fn is_review_template_azure(_path_str: &str) -> bool { + // TODO: implement + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_review_template_github() { + assert!(is_review_template_github("PULL_REQUEST_TEMPLATE.md")); + assert!(is_review_template_github("pull_request_template.md")); + assert!(is_review_template_github("PULL_REQUEST_TEMPLATE/")); + assert!(!is_review_template_github("README.md")); + } +} diff --git a/crates/gitbutler-project/Cargo.toml b/crates/gitbutler-project/Cargo.toml index 5eed6f97b..4d29dee9f 100644 --- a/crates/gitbutler-project/Cargo.toml +++ b/crates/gitbutler-project/Cargo.toml @@ -14,6 +14,7 @@ gitbutler-error.workspace = true gitbutler-serde.workspace = true gitbutler-id.workspace = true gitbutler-storage.workspace = true +gitbutler-forge.workspace = true git2.workspace = true gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] } uuid.workspace = true diff --git a/crates/gitbutler-project/src/project.rs b/crates/gitbutler-project/src/project.rs index ce1a1d12f..c68047c03 100644 --- a/crates/gitbutler-project/src/project.rs +++ b/crates/gitbutler-project/src/project.rs @@ -1,8 +1,9 @@ use std::{ - path::{self, PathBuf}, + path::{self, Path, PathBuf}, time, }; +use gitbutler_forge::{forge::ForgeType, review::available_review_templates}; use gitbutler_id::id::Id; use serde::{Deserialize, Serialize}; @@ -102,9 +103,21 @@ pub struct Project { #[serde(rename_all = "camelCase")] pub struct GitHostSettings { #[serde(default)] - pub host_type: Option, + pub host_type: Option, #[serde(default)] - pub pull_request_template_path: Option, + pub review_template_path: Option, +} + +impl GitHostSettings { + pub fn init(&mut self, project_path: &Path) { + if let Some(forge_type) = &self.host_type { + if self.review_template_path.is_none() { + self.review_template_path = available_review_templates(project_path, forge_type) + .first() + .cloned(); + } + } + } } impl Project { diff --git a/crates/gitbutler-tauri/Cargo.toml b/crates/gitbutler-tauri/Cargo.toml index e9d3d7bf2..4fe5077bc 100644 --- a/crates/gitbutler-tauri/Cargo.toml +++ b/crates/gitbutler-tauri/Cargo.toml @@ -73,6 +73,7 @@ gitbutler-diff.workspace = true gitbutler-operating-modes.workspace = true gitbutler-edit-mode.workspace = true gitbutler-sync.workspace = true +gitbutler-forge.workspace = true open = "5" url = "2.5.2" diff --git a/crates/gitbutler-tauri/src/forge.rs b/crates/gitbutler-tauri/src/forge.rs new file mode 100644 index 000000000..85edff0a9 --- /dev/null +++ b/crates/gitbutler-tauri/src/forge.rs @@ -0,0 +1,24 @@ +pub mod commands { + use gitbutler_forge::review::available_review_templates; + use gitbutler_project::{Controller, ProjectId}; + use tauri::State; + use tracing::instrument; + + use crate::error::Error; + + #[tauri::command(async)] + #[instrument(skip(projects), err(Debug))] + pub fn get_available_review_templates( + projects: State<'_, Controller>, + project_id: ProjectId, + ) -> Result, Error> { + let project = projects.get_validated(project_id)?; + let root_path = &project.path; + let forge_type = project.git_host.host_type; + + let review_templates = forge_type + .map(|forge_type| available_review_templates(root_path, &forge_type)) + .unwrap_or_default(); + Ok(review_templates) + } +} diff --git a/crates/gitbutler-tauri/src/github.rs b/crates/gitbutler-tauri/src/github.rs index 74b2bf8b2..1a1041780 100644 --- a/crates/gitbutler-tauri/src/github.rs +++ b/crates/gitbutler-tauri/src/github.rs @@ -1,8 +1,7 @@ pub mod commands { - use std::{collections::HashMap, path}; + use std::collections::HashMap; use anyhow::{Context, Result}; - use gitbutler_fs::list_files; use serde::{Deserialize, Serialize}; use tracing::instrument; @@ -80,28 +79,4 @@ pub mod commands { .context("Failed to parse response body") .map_err(Into::into) } - - #[tauri::command(async)] - #[instrument] - pub fn available_pull_request_templates(root_path: &path::Path) -> Result, Error> { - let walked_paths = list_files(root_path, &[root_path])?; - - let mut available_paths = Vec::new(); - for entry in walked_paths { - let path_entry = entry.as_path(); - let path_str = path_entry.to_string_lossy(); - // TODO: Refactor these paths out in the future to something like a common - // gitHosts.pullRequestTemplatePaths map, an entry for each gitHost type and - // their valid files / directories. So that this 'get_available_templates' - // can be more generic and we can add / modify paths more easily for all supported githost types - if path_str == "PULL_REQUEST_TEMPLATE.md" - || path_str == "pull_request_template.md" - || path_str.contains("PULL_REQUEST_TEMPLATE/") - { - available_paths.push(root_path.join(path_entry).to_string_lossy().to_string()); - } - } - - Ok(available_paths) - } } diff --git a/crates/gitbutler-tauri/src/lib.rs b/crates/gitbutler-tauri/src/lib.rs index 64695932d..e0cc58afb 100644 --- a/crates/gitbutler-tauri/src/lib.rs +++ b/crates/gitbutler-tauri/src/lib.rs @@ -24,6 +24,7 @@ pub use window::state::WindowState; pub mod askpass; pub mod config; pub mod error; +pub mod forge; pub mod github; pub mod modes; pub mod open; diff --git a/crates/gitbutler-tauri/src/main.rs b/crates/gitbutler-tauri/src/main.rs index fe8547df7..2c9b1d48a 100644 --- a/crates/gitbutler-tauri/src/main.rs +++ b/crates/gitbutler-tauri/src/main.rs @@ -12,8 +12,8 @@ )] use gitbutler_tauri::{ - askpass, commands, config, github, logs, menu, modes, open, projects, remotes, repo, secret, - stack, undo, users, virtual_branches, zip, App, WindowState, + askpass, commands, config, forge, github, logs, menu, modes, open, projects, remotes, repo, + secret, stack, undo, users, virtual_branches, zip, App, WindowState, }; use tauri::{generate_context, Manager}; use tauri_plugin_log::LogTarget; @@ -146,6 +146,7 @@ fn main() { projects::commands::list_projects, projects::commands::set_project_active, projects::commands::open_project_in_window, + projects::commands::update_project_git_host, repo::commands::git_get_local_config, repo::commands::git_set_local_config, repo::commands::check_signing_settings, @@ -207,7 +208,6 @@ fn main() { menu::get_editor_link_scheme, github::commands::init_device_oauth, github::commands::check_auth_status, - github::commands::available_pull_request_templates, askpass::commands::submit_prompt_response, remotes::list_remotes, remotes::add_remote, @@ -216,7 +216,8 @@ fn main() { modes::save_edit_and_return_to_workspace, modes::abort_edit_and_return_to_workspace, modes::edit_initial_index_state, - open::open_url + open::open_url, + forge::commands::get_available_review_templates, ]) .menu(menu::build(tauri_context.package_info())) .on_menu_event(|event| menu::handle_event(&event)) diff --git a/crates/gitbutler-tauri/src/projects.rs b/crates/gitbutler-tauri/src/projects.rs index c19ca3da3..2b1d72081 100644 --- a/crates/gitbutler-tauri/src/projects.rs +++ b/crates/gitbutler-tauri/src/projects.rs @@ -4,7 +4,9 @@ pub mod commands { use std::path; use anyhow::Context; - use gitbutler_project::{self as projects, Controller, ProjectId}; + use gitbutler_project::{ + self as projects, Controller, GitHostSettings, ProjectId, UpdateRequest, + }; use tauri::{State, Window}; use tracing::instrument; @@ -19,6 +21,26 @@ pub mod commands { Ok(projects.update(&project)?) } + #[tauri::command(async)] + #[instrument(skip(projects), err(Debug))] + pub fn update_project_git_host( + projects: State<'_, Controller>, + project_id: ProjectId, + git_host: GitHostSettings, + ) -> Result { + let project = projects.get_validated(project_id)?; + let root_path = &project.path; + let mut git_host = git_host.clone(); + git_host.init(root_path); + + let request = UpdateRequest { + id: project_id, + git_host: Some(git_host), + ..Default::default() + }; + Ok(projects.update(&request)?) + } + #[tauri::command(async)] #[instrument(skip(projects), err(Debug))] pub fn add_project(