gitbulter-forge crate

Create a centralized crate for forge actions that is provider agnostic.
Move the logic behind fetching the PR templates to it.
This commit is contained in:
estib 2024-10-17 16:23:56 +02:00
parent e37275e8c9
commit 4ee01031f6
14 changed files with 211 additions and 34 deletions

11
Cargo.lock generated
View File

@ -2339,6 +2339,15 @@ dependencies = [
"zip", "zip",
] ]
[[package]]
name = "gitbutler-forge"
version = "0.0.0"
dependencies = [
"anyhow",
"gitbutler-fs",
"serde",
]
[[package]] [[package]]
name = "gitbutler-fs" name = "gitbutler-fs"
version = "0.0.0" version = "0.0.0"
@ -2464,6 +2473,7 @@ dependencies = [
"fslock", "fslock",
"git2", "git2",
"gitbutler-error", "gitbutler-error",
"gitbutler-forge",
"gitbutler-id", "gitbutler-id",
"gitbutler-serde", "gitbutler-serde",
"gitbutler-storage", "gitbutler-storage",
@ -2648,6 +2658,7 @@ dependencies = [
"gitbutler-edit-mode", "gitbutler-edit-mode",
"gitbutler-error", "gitbutler-error",
"gitbutler-feedback", "gitbutler-feedback",
"gitbutler-forge",
"gitbutler-fs", "gitbutler-fs",
"gitbutler-id", "gitbutler-id",
"gitbutler-operating-modes", "gitbutler-operating-modes",

View File

@ -92,6 +92,7 @@ gitbutler-oxidize = { path = "crates/gitbutler-oxidize" }
gitbutler-stack-api = { path = "crates/gitbutler-stack-api" } gitbutler-stack-api = { path = "crates/gitbutler-stack-api" }
gitbutler-stack = { path = "crates/gitbutler-stack" } gitbutler-stack = { path = "crates/gitbutler-stack" }
gitbutler-patch-reference = { path = "crates/gitbutler-patch-reference" } gitbutler-patch-reference = { path = "crates/gitbutler-patch-reference" }
gitbutler-forge = { path = "crates/gitbutler-forge" }
[profile.release] [profile.release]
codegen-units = 1 # Compile crates one after another so the compiler can optimize better codegen-units = 1 # Compile crates one after another so the compiler can optimize better

View File

@ -0,0 +1,11 @@
[package]
name = "gitbutler-forge"
version = "0.0.0"
edition = "2021"
authors = ["GitButler <gitbutler@gitbutler.com>"]
publish = false
[dependencies]
serde = { workspace = true, features = ["std"] }
anyhow = "1.0.86"
gitbutler-fs.workspace = true

View File

@ -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,
}

View File

@ -0,0 +1,2 @@
pub mod forge;
pub mod review;

View File

@ -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<String> {
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"));
}
}

View File

@ -14,6 +14,7 @@ gitbutler-error.workspace = true
gitbutler-serde.workspace = true gitbutler-serde.workspace = true
gitbutler-id.workspace = true gitbutler-id.workspace = true
gitbutler-storage.workspace = true gitbutler-storage.workspace = true
gitbutler-forge.workspace = true
git2.workspace = true git2.workspace = true
gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] } gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] }
uuid.workspace = true uuid.workspace = true

View File

@ -1,8 +1,9 @@
use std::{ use std::{
path::{self, PathBuf}, path::{self, Path, PathBuf},
time, time,
}; };
use gitbutler_forge::{forge::ForgeType, review::available_review_templates};
use gitbutler_id::id::Id; use gitbutler_id::id::Id;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -102,9 +103,21 @@ pub struct Project {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GitHostSettings { pub struct GitHostSettings {
#[serde(default)] #[serde(default)]
pub host_type: Option<String>, pub host_type: Option<ForgeType>,
#[serde(default)] #[serde(default)]
pub pull_request_template_path: Option<String>, pub review_template_path: Option<String>,
}
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 { impl Project {

View File

@ -73,6 +73,7 @@ gitbutler-diff.workspace = true
gitbutler-operating-modes.workspace = true gitbutler-operating-modes.workspace = true
gitbutler-edit-mode.workspace = true gitbutler-edit-mode.workspace = true
gitbutler-sync.workspace = true gitbutler-sync.workspace = true
gitbutler-forge.workspace = true
open = "5" open = "5"
url = "2.5.2" url = "2.5.2"

View File

@ -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<Vec<String>, 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)
}
}

View File

@ -1,8 +1,7 @@
pub mod commands { pub mod commands {
use std::{collections::HashMap, path}; use std::collections::HashMap;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use gitbutler_fs::list_files;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::instrument; use tracing::instrument;
@ -80,28 +79,4 @@ pub mod commands {
.context("Failed to parse response body") .context("Failed to parse response body")
.map_err(Into::into) .map_err(Into::into)
} }
#[tauri::command(async)]
#[instrument]
pub fn available_pull_request_templates(root_path: &path::Path) -> Result<Vec<String>, 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)
}
} }

View File

@ -24,6 +24,7 @@ pub use window::state::WindowState;
pub mod askpass; pub mod askpass;
pub mod config; pub mod config;
pub mod error; pub mod error;
pub mod forge;
pub mod github; pub mod github;
pub mod modes; pub mod modes;
pub mod open; pub mod open;

View File

@ -12,8 +12,8 @@
)] )]
use gitbutler_tauri::{ use gitbutler_tauri::{
askpass, commands, config, github, logs, menu, modes, open, projects, remotes, repo, secret, askpass, commands, config, forge, github, logs, menu, modes, open, projects, remotes, repo,
stack, undo, users, virtual_branches, zip, App, WindowState, secret, stack, undo, users, virtual_branches, zip, App, WindowState,
}; };
use tauri::{generate_context, Manager}; use tauri::{generate_context, Manager};
use tauri_plugin_log::LogTarget; use tauri_plugin_log::LogTarget;
@ -146,6 +146,7 @@ fn main() {
projects::commands::list_projects, projects::commands::list_projects,
projects::commands::set_project_active, projects::commands::set_project_active,
projects::commands::open_project_in_window, projects::commands::open_project_in_window,
projects::commands::update_project_git_host,
repo::commands::git_get_local_config, repo::commands::git_get_local_config,
repo::commands::git_set_local_config, repo::commands::git_set_local_config,
repo::commands::check_signing_settings, repo::commands::check_signing_settings,
@ -207,7 +208,6 @@ fn main() {
menu::get_editor_link_scheme, menu::get_editor_link_scheme,
github::commands::init_device_oauth, github::commands::init_device_oauth,
github::commands::check_auth_status, github::commands::check_auth_status,
github::commands::available_pull_request_templates,
askpass::commands::submit_prompt_response, askpass::commands::submit_prompt_response,
remotes::list_remotes, remotes::list_remotes,
remotes::add_remote, remotes::add_remote,
@ -216,7 +216,8 @@ fn main() {
modes::save_edit_and_return_to_workspace, modes::save_edit_and_return_to_workspace,
modes::abort_edit_and_return_to_workspace, modes::abort_edit_and_return_to_workspace,
modes::edit_initial_index_state, modes::edit_initial_index_state,
open::open_url open::open_url,
forge::commands::get_available_review_templates,
]) ])
.menu(menu::build(tauri_context.package_info())) .menu(menu::build(tauri_context.package_info()))
.on_menu_event(|event| menu::handle_event(&event)) .on_menu_event(|event| menu::handle_event(&event))

View File

@ -4,7 +4,9 @@ pub mod commands {
use std::path; use std::path;
use anyhow::Context; 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 tauri::{State, Window};
use tracing::instrument; use tracing::instrument;
@ -19,6 +21,26 @@ pub mod commands {
Ok(projects.update(&project)?) 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<projects::Project, Error> {
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)] #[tauri::command(async)]
#[instrument(skip(projects), err(Debug))] #[instrument(skip(projects), err(Debug))]
pub fn add_project( pub fn add_project(