Skip to content

Commit 74e7e22

Browse files
committed
Creates frontend APIs for accessing, creating and modifying workspace rules
1 parent 8e3652c commit 74e7e22

File tree

7 files changed

+180
-7
lines changed

7 files changed

+180
-7
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/but-rules/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ serde_regex = "1.1.0"
1919
serde_json = "1.0.138"
2020
gitbutler-command-context.workspace = true
2121
but-db.workspace = true
22+
uuid.workspace = true

crates/but-rules/src/lib.rs

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use gitbutler_command_context::CommandContext;
12
use serde::{Deserialize, Serialize};
23

34
pub mod db;
@@ -31,7 +32,7 @@ pub enum Trigger {
3132
/// A filter is a condition that determines what files or changes the rule applies to.
3233
/// Within a filter, multiple conditions are combined with AND logic (i.e. to match all conditions must be met)
3334
#[derive(Serialize, Deserialize, Debug, Clone)]
34-
#[serde(rename_all = "camelCase")]
35+
#[serde(rename_all = "camelCase", tag = "type", content = "subject")]
3536
pub enum Filter {
3637
/// Matches the file path (relative to the repository root) against all provided regex patterns.
3738
#[serde(with = "serde_regex")]
@@ -63,7 +64,7 @@ pub enum TreeStatus {
6364
/// Represents a semantic type of change that was inferred for the change.
6465
/// Typically this means a heuristic or an LLM determinded that a change represents a refactor, a new feature, a bug fix, or documentation update.
6566
#[derive(Serialize, Deserialize, Debug, Clone)]
66-
#[serde(rename_all = "camelCase")]
67+
#[serde(rename_all = "camelCase", tag = "type", content = "subject")]
6768
pub enum SemanticType {
6869
/// A change that is a refactor, meaning it does not change the external behavior of the code but improves its structure.
6970
Refactor,
@@ -81,7 +82,7 @@ pub enum SemanticType {
8182
/// An action can be either explicit, meaning the user defined something like "Assign in Lane A" or "Ammend into Commit X"
8283
/// or it is implicit, meaning the action was determined by heuristics or AI, such as "Assign to appropriate branch" or "Absorb in dependent commit".
8384
#[derive(Serialize, Deserialize, Debug, Clone)]
84-
#[serde(rename_all = "camelCase")]
85+
#[serde(rename_all = "camelCase", tag = "type", content = "subject")]
8586
pub enum Action {
8687
/// An action that has an explicit operation defined by the user.
8788
Explicit(Operation),
@@ -91,7 +92,7 @@ pub enum Action {
9192

9293
/// Represents the operation that a user can configure to be performed in an explicit action.
9394
#[derive(Serialize, Deserialize, Debug, Clone)]
94-
#[serde(rename_all = "camelCase")]
95+
#[serde(rename_all = "camelCase", tag = "type", content = "subject")]
9596
pub enum Operation {
9697
/// Assign the matched changes to a specific stack ID.
9798
Assign { stack_id: String },
@@ -103,7 +104,7 @@ pub enum Operation {
103104

104105
/// Represents the implicit operation that is determined by heuristics or AI.
105106
#[derive(Serialize, Deserialize, Debug, Clone)]
106-
#[serde(rename_all = "camelCase")]
107+
#[serde(rename_all = "camelCase", tag = "type", content = "subject")]
107108
pub enum ImplicitOperation {
108109
/// Assign the matched changes to the appropriate branch based on offline heuristics.
109110
AssignToAppropriateBranch,
@@ -112,3 +113,108 @@ pub enum ImplicitOperation {
112113
/// Perform an operation based on LLM-driven analysis and tool calling.
113114
LLMPrompt(String),
114115
}
116+
117+
/// A request to create a new workspace rule.
118+
#[derive(Serialize, Deserialize, Debug, Clone)]
119+
#[serde(rename_all = "camelCase")]
120+
pub struct CreateRuleRequest {
121+
/// The trigger that causes the rule to be evaluated.
122+
pub trigger: Trigger,
123+
/// The filters that determine what files or changes the rule applies to. Can not be empty.
124+
pub filters: Vec<Filter>,
125+
/// The action that determines what happens to the files or changes that matched the filters.
126+
pub action: Action,
127+
}
128+
129+
/// Creates a new workspace rule
130+
pub fn create_rule(
131+
ctx: &mut CommandContext,
132+
req: CreateRuleRequest,
133+
) -> anyhow::Result<WorkspaceRule> {
134+
if req.filters.is_empty() {
135+
return Err(anyhow::anyhow!("At least one filter is required"));
136+
}
137+
let rule = WorkspaceRule {
138+
id: uuid::Uuid::new_v4().to_string(),
139+
created_at: chrono::Local::now().naive_local(),
140+
enabled: true,
141+
trigger: req.trigger,
142+
filters: req.filters,
143+
action: req.action,
144+
};
145+
146+
ctx.db()?
147+
.workspace_rules()
148+
.insert(rule.clone().try_into()?)
149+
.map_err(|e| anyhow::anyhow!("Failed to insert workspace rule: {}", e))?;
150+
Ok(rule)
151+
}
152+
153+
/// Deletes an existing workspace rule by its ID.
154+
pub fn delete_rule(ctx: &mut CommandContext, id: &str) -> anyhow::Result<()> {
155+
ctx.db()?
156+
.workspace_rules()
157+
.delete(id)
158+
.map_err(|e| anyhow::anyhow!("Failed to delete workspace rule: {}", e))?;
159+
Ok(())
160+
}
161+
162+
/// A request to update an existing workspace rule.
163+
#[derive(Serialize, Deserialize, Debug, Clone)]
164+
#[serde(rename_all = "camelCase")]
165+
pub struct UpdateRuleRequest {
166+
/// The ID of the rule to update.
167+
id: String,
168+
/// The new enabled state of the rule. If not provided, the existing state is retained.
169+
enabled: Option<bool>,
170+
/// The new trigger for the rule. If not provided, the existing trigger is retained.
171+
trigger: Option<Trigger>,
172+
/// The new filters for the rule. If not provided, the existing filters are retained.
173+
filters: Option<Vec<Filter>>,
174+
/// The new action for the rule. If not provided, the existing action is retained.
175+
action: Option<Action>,
176+
}
177+
178+
/// Updates an existing workspace rule with the provided request data.
179+
pub fn update_rule(
180+
ctx: &mut CommandContext,
181+
req: UpdateRuleRequest,
182+
) -> anyhow::Result<WorkspaceRule> {
183+
let mut rule: WorkspaceRule = ctx
184+
.db()?
185+
.workspace_rules()
186+
.get(&req.id)?
187+
.ok_or_else(|| anyhow::anyhow!("Rule with ID {} not found", req.id))?
188+
.try_into()?;
189+
190+
if let Some(enabled) = req.enabled {
191+
rule.enabled = enabled;
192+
}
193+
if let Some(trigger) = req.trigger {
194+
rule.trigger = trigger;
195+
}
196+
if let Some(filters) = req.filters {
197+
rule.filters = filters;
198+
}
199+
if let Some(action) = req.action {
200+
rule.action = action;
201+
}
202+
203+
ctx.db()?
204+
.workspace_rules()
205+
.update(&req.id, rule.clone().try_into()?)
206+
.map_err(|e| anyhow::anyhow!("Failed to update workspace rule: {}", e))?;
207+
Ok(rule)
208+
}
209+
210+
/// Lists all workspace rules in the database.
211+
pub fn list_rules(ctx: &mut CommandContext) -> anyhow::Result<Vec<WorkspaceRule>> {
212+
let rules = ctx
213+
.db()?
214+
.workspace_rules()
215+
.list()?
216+
.into_iter()
217+
.map(|r| r.try_into())
218+
.collect::<Result<Vec<WorkspaceRule>, _>>()?;
219+
Ok(rules)
220+
}

crates/gitbutler-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ but-graph.workspace = true
8282
but-hunk-dependency.workspace = true
8383
but-hunk-assignment.workspace = true
8484
but-action.workspace = true
85+
but-rules.workspace = true
8586
open = "5"
8687
url = "2.5.4"
8788
dirs = "6.0.0"

crates/gitbutler-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub mod open;
3434
pub mod projects;
3535
pub mod remotes;
3636
pub mod repo;
37+
pub mod rules;
3738
pub mod secret;
3839
pub mod undo;
3940
pub mod users;

crates/gitbutler-tauri/src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use gitbutler_tauri::csp::csp_with_extras;
1616
use gitbutler_tauri::settings::SettingsStore;
1717
use gitbutler_tauri::{
1818
action, askpass, cli, commands, config, diff, env, forge, github, logs, menu, modes, open,
19-
projects, remotes, repo, secret, settings, stack, undo, users, virtual_branches, workspace,
20-
zip, App, WindowState,
19+
projects, remotes, repo, rules, secret, settings, stack, undo, users, virtual_branches,
20+
workspace, zip, App, WindowState,
2121
};
2222
use tauri::Emitter;
2323
use tauri::{generate_context, Manager};
@@ -283,6 +283,10 @@ fn main() {
283283
action::freestyle,
284284
cli::install_cli,
285285
cli::cli_path,
286+
rules::create_workspace_rule,
287+
rules::delete_workspace_rule,
288+
rules::update_workspace_rule,
289+
rules::list_workspace_rules,
286290
workspace::stacks,
287291
workspace::stack_details,
288292
workspace::branch_details,

crates/gitbutler-tauri/src/rules.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use crate::error::Error;
2+
use but_rules::{
3+
create_rule, delete_rule, list_rules, update_rule, CreateRuleRequest, UpdateRuleRequest,
4+
WorkspaceRule,
5+
};
6+
use but_settings::AppSettingsWithDiskSync;
7+
use gitbutler_command_context::CommandContext;
8+
use gitbutler_project as projects;
9+
use gitbutler_project::ProjectId;
10+
use tauri::State;
11+
use tracing::instrument;
12+
13+
#[tauri::command(async)]
14+
#[instrument(skip(projects, settings), err(Debug))]
15+
pub fn create_workspace_rule(
16+
projects: State<'_, projects::Controller>,
17+
settings: State<'_, AppSettingsWithDiskSync>,
18+
project_id: ProjectId,
19+
request: CreateRuleRequest,
20+
) -> Result<WorkspaceRule, Error> {
21+
let ctx = &mut CommandContext::open(&projects.get(project_id)?, settings.get()?.clone())?;
22+
create_rule(ctx, request).map_err(Into::into)
23+
}
24+
25+
#[tauri::command(async)]
26+
#[instrument(skip(projects, settings), err(Debug))]
27+
pub fn delete_workspace_rule(
28+
projects: State<'_, projects::Controller>,
29+
settings: State<'_, AppSettingsWithDiskSync>,
30+
project_id: ProjectId,
31+
id: String,
32+
) -> Result<(), Error> {
33+
let ctx = &mut CommandContext::open(&projects.get(project_id)?, settings.get()?.clone())?;
34+
delete_rule(ctx, &id).map_err(Into::into)
35+
}
36+
37+
#[tauri::command(async)]
38+
#[instrument(skip(projects, settings), err(Debug))]
39+
pub fn update_workspace_rule(
40+
projects: State<'_, projects::Controller>,
41+
settings: State<'_, AppSettingsWithDiskSync>,
42+
project_id: ProjectId,
43+
request: UpdateRuleRequest,
44+
) -> Result<WorkspaceRule, Error> {
45+
let ctx = &mut CommandContext::open(&projects.get(project_id)?, settings.get()?.clone())?;
46+
update_rule(ctx, request).map_err(Into::into)
47+
}
48+
49+
#[tauri::command(async)]
50+
#[instrument(skip(projects, settings), err(Debug))]
51+
pub fn list_workspace_rules(
52+
projects: State<'_, projects::Controller>,
53+
settings: State<'_, AppSettingsWithDiskSync>,
54+
project_id: ProjectId,
55+
) -> Result<Vec<WorkspaceRule>, Error> {
56+
let ctx = &mut CommandContext::open(&projects.get(project_id)?, settings.get()?.clone())?;
57+
list_rules(ctx).map_err(Into::into)
58+
}

0 commit comments

Comments
 (0)