Skip to content

Commit f6b4627

Browse files
committed
Workspace rules: add model, service & integration
- Add new rule definition and service files for workspace rules - Integrate rulesService into +layout.svelte for Svelte context - Update ReduxTag enum to include WorkspaceRules - Exclude backendService implementation from this commit
1 parent e9b0597 commit f6b4627

File tree

4 files changed

+195
-1
lines changed

4 files changed

+195
-1
lines changed

apps/desktop/src/lib/rules/rule.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { BrandedId } from '@gitbutler/shared/utils/branding';
2+
3+
export type WorkspaceRuleId = BrandedId<'WorkspaceRule'>;
4+
/**
5+
* A workspace rule.
6+
* @remarks
7+
* A rule is evaluated in the app and determines what happens to files or changes based on triggers, filters, and actions.
8+
*/
9+
export interface WorkspaceRule {
10+
/** A UUID unique identifier for the rule. */
11+
id: WorkspaceRuleId;
12+
/** The time when the rule was created, represented as a Unix timestamp in milliseconds. */
13+
createdAt: string; // ISO string or Date, depending on backend serialization
14+
/** Whether the rule is currently enabled or not. */
15+
enabled: boolean;
16+
/** The trigger of the rule is what causes it to be evaluated in the app. */
17+
trigger: Trigger;
18+
/** These filters determine what files or changes the rule applies to. Multiple filters are combined with OR logic. */
19+
filters: Filter[];
20+
/** The action determines what happens to the files or changes that matched the filters. */
21+
action: Action;
22+
}
23+
24+
/**
25+
* Represents the kinds of events in the app that can cause a rule to be evaluated.
26+
*/
27+
export type Trigger =
28+
/** When a file is added, removed or modified in the Git worktree. */
29+
'fileSytemChange';
30+
31+
/**
32+
* A filter is a condition that determines what files or changes the rule applies to.
33+
* Multiple conditions in a filter are combined with AND logic.
34+
*/
35+
export type Filter =
36+
| { type: 'pathMatchesRegex'; subject: string[] } // regex patterns as strings
37+
| { type: 'contentMatchesRegex'; subject: string[] } // regex patterns as strings
38+
| { type: 'fileChangeType'; subject: TreeStatus }
39+
| { type: 'semanticType'; subject: SemanticType };
40+
41+
/**
42+
* Represents the type of change that occurred in the Git worktree.
43+
* Matches the TreeStatus of the TreeChange.
44+
*/
45+
export type TreeStatus = 'addition' | 'deletion' | 'modification' | 'rename';
46+
47+
/**
48+
* Represents a semantic type of change that was inferred for the change.
49+
* Typically this means a heuristic or an LLM determined that a change represents a refactor, a new feature, a bug fix, or documentation update.
50+
*/
51+
export type SemanticType =
52+
| { type: 'refactor' }
53+
| { type: 'newFeature' }
54+
| { type: 'bugFix' }
55+
| { type: 'documentation' }
56+
| { type: 'userDefined'; subject: string };
57+
58+
/**
59+
* Represents an action that can be taken based on the rule evaluation.
60+
* An action can be either explicit (user defined) or implicit (determined by heuristics or AI).
61+
*/
62+
export type Action =
63+
| { type: 'explicit'; subject: Operation }
64+
| { type: 'implicit'; subject: ImplicitOperation };
65+
66+
/**
67+
* Represents the operation that a user can configure to be performed in an explicit action.
68+
*/
69+
export type Operation =
70+
| { type: 'assign'; stackId: string }
71+
| { type: 'amend'; commitId: string }
72+
| { type: 'newCommit'; branchName: string };
73+
74+
/**
75+
* Represents the implicit operation that is determined by heuristics or AI.
76+
*/
77+
export type ImplicitOperation =
78+
| { type: 'assignToAppropriateBranch' }
79+
| { type: 'absorbIntoDependentCommit' }
80+
| { type: 'llmPrompt'; subject: string };
81+
82+
/**
83+
* A request to create a new workspace rule.
84+
*/
85+
export interface CreateRuleRequest {
86+
/** The trigger that causes the rule to be evaluated. */
87+
trigger: Trigger;
88+
/** The filters that determine what files or changes the rule applies to. Cannot be empty. */
89+
filters: Filter[];
90+
/** The action that determines what happens to the files or changes that matched the filters. */
91+
action: Action;
92+
}
93+
94+
/**
95+
* A request to update an existing workspace rule.
96+
*/
97+
export interface UpdateRuleRequest {
98+
/** The ID of the rule to update. */
99+
id: WorkspaceRuleId;
100+
/** The new enabled state of the rule. If not provided, the existing state is retained. */
101+
enabled: boolean | null;
102+
/** The new trigger for the rule. If not provided, the existing trigger is retained. */
103+
trigger: Trigger | null;
104+
/** The new filters for the rule. If not provided, the existing filters are retained. */
105+
filters: Filter[] | null;
106+
/** The new action for the rule. If not provided, the existing action is retained. */
107+
action: Action | null;
108+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { invalidatesItem, invalidatesList, providesItems, ReduxTag } from '$lib/state/tags';
2+
import { createEntityAdapter, type EntityState } from '@reduxjs/toolkit';
3+
import type {
4+
CreateRuleRequest,
5+
UpdateRuleRequest,
6+
WorkspaceRule,
7+
WorkspaceRuleId
8+
} from '$lib/rules/rule';
9+
import type { BackendApi } from '$lib/state/clientState.svelte';
10+
11+
export default class RulesService {
12+
private api: ReturnType<typeof injectEndpoints>;
13+
14+
constructor(backendApi: BackendApi) {
15+
this.api = injectEndpoints(backendApi);
16+
}
17+
18+
get createWorkspaceRule() {
19+
return this.api.endpoints.createWorkspaceRule.useMutation;
20+
}
21+
22+
get deleteWorkspaceRule() {
23+
return this.api.endpoints.deleteWorkspaceRule.useMutation;
24+
}
25+
26+
get updateWorkspaceRule() {
27+
return this.api.endpoints.updateWorkspaceRule.useMutation;
28+
}
29+
30+
get listWorkspaceRules() {
31+
return this.api.endpoints.listWorkspaceRules.useQuery;
32+
}
33+
}
34+
35+
function injectEndpoints(api: BackendApi) {
36+
return api.injectEndpoints({
37+
endpoints: (build) => ({
38+
createWorkspaceRule: build.mutation<
39+
WorkspaceRule,
40+
{ projectId: string; request: CreateRuleRequest }
41+
>({
42+
extraOptions: { command: 'create_workspace_rule' },
43+
query: (args) => args,
44+
invalidatesTags: () => [invalidatesList(ReduxTag.WorkspaceRules)]
45+
}),
46+
deleteWorkspaceRule: build.mutation<void, { projectId: string; ruleId: WorkspaceRuleId }>({
47+
extraOptions: { command: 'delete_workspace_rule' },
48+
query: (args) => args,
49+
invalidatesTags: () => [invalidatesList(ReduxTag.WorkspaceRules)]
50+
}),
51+
updateWorkspaceRule: build.mutation<
52+
WorkspaceRule,
53+
{ projectId: string; request: UpdateRuleRequest }
54+
>({
55+
extraOptions: { command: 'update_workspace_rule' },
56+
query: (args) => args,
57+
invalidatesTags: (result) =>
58+
result
59+
? [
60+
invalidatesItem(ReduxTag.WorkspaceRules, result.id),
61+
invalidatesList(ReduxTag.WorkspaceRules)
62+
]
63+
: []
64+
}),
65+
listWorkspaceRules: build.query<
66+
EntityState<WorkspaceRule, WorkspaceRuleId>,
67+
{ projectId: string }
68+
>({
69+
extraOptions: { command: 'list_workspace_rules' },
70+
query: (args) => args,
71+
providesTags: (result) => providesItems(ReduxTag.WorkspaceRules, result?.ids ?? []),
72+
transformResponse: (response: WorkspaceRule[]) => {
73+
return workspaceRulesAdapter.addMany(workspaceRulesAdapter.getInitialState(), response);
74+
}
75+
})
76+
})
77+
});
78+
}
79+
80+
const workspaceRulesAdapter = createEntityAdapter<WorkspaceRule, WorkspaceRuleId>({
81+
selectId: (rule) => rule.id
82+
});

apps/desktop/src/lib/state/tags.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export enum ReduxTag {
2020
UpstreamIntegrationStatus = 'UpstreamIntegrationStatus',
2121
BranchListing = 'BranchListing',
2222
BranchDetails = 'BranchDetails',
23-
SnapshotDiff = 'SnapshotDiff'
23+
SnapshotDiff = 'SnapshotDiff',
24+
WorkspaceRules = 'WorkspaceRules'
2425
}
2526

2627
type Tag<T extends string | number> = {

apps/desktop/src/routes/+layout.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import { ProjectsService } from '$lib/project/projectsService';
5151
import { PromptService } from '$lib/prompt/promptService';
5252
import { RemotesService } from '$lib/remotes/remotesService';
53+
import RulesService from '$lib/rules/rulesService.svelte';
5354
import { setSecretsService } from '$lib/secrets/secretsService';
5455
import { IdSelection } from '$lib/selection/idSelection.svelte';
5556
import { UncommittedService } from '$lib/selection/uncommittedService.svelte';
@@ -156,6 +157,7 @@
156157
forgeFactory,
157158
uiState
158159
);
160+
const rulesService = new RulesService(clientState['backendApi']);
159161
const actionService = new ActionService(clientState['backendApi']);
160162
const oplogService = new OplogService(clientState['backendApi']);
161163
const baseBranchService = new BaseBranchService(clientState.backendApi);
@@ -251,6 +253,7 @@
251253
setContext(AppSettings, data.appSettings);
252254
setContext(EventContext, data.eventContext);
253255
setContext(StackService, stackService);
256+
setContext(RulesService, rulesService);
254257
setContext(ActionService, actionService);
255258
setContext(OplogService, oplogService);
256259
setContext(BaseBranchService, baseBranchService);

0 commit comments

Comments
 (0)