387 lines
18 KiB
Go
387 lines
18 KiB
Go
package gitlab
|
|
|
|
import "time"
|
|
|
|
// ProjectFilter identifies a GitLab project for review/issue watch filtering.
|
|
// Path is the namespace/path slug, e.g. "group/sub/project".
|
|
type ProjectFilter struct {
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
// ReviewScope controls which reviewer_username scope is used for the review
|
|
// watch. GitLab does not have team-level review requests, so the scope set is
|
|
// smaller than GitHub's, but we keep the constant set parallel for symmetry.
|
|
const (
|
|
// ReviewScopeUser matches MRs where the user is explicitly the reviewer.
|
|
ReviewScopeUser = "user"
|
|
// ReviewScopeUserAndTeams is treated identically to ReviewScopeUser on
|
|
// GitLab (no team review requests) — kept for cross-integration symmetry.
|
|
ReviewScopeUserAndTeams = "user_and_teams"
|
|
)
|
|
|
|
// CleanupPolicy controls how a review or issue watch handles its auto-created
|
|
// tasks once the underlying MR / issue reaches a terminal state.
|
|
const (
|
|
// CleanupPolicyAuto deletes the task once the MR/issue is merged or closed
|
|
// UNLESS the user authored at least one message in the task (the agent's
|
|
// auto-start prompt does not count).
|
|
CleanupPolicyAuto = "auto"
|
|
// CleanupPolicyAlways deletes the task on terminal state regardless of
|
|
// user interaction.
|
|
CleanupPolicyAlways = "always"
|
|
// CleanupPolicyNever disables automatic cleanup.
|
|
CleanupPolicyNever = "never"
|
|
)
|
|
|
|
// IsValidCleanupPolicy reports whether s is one of the recognized policies.
|
|
// Empty string is treated as valid so legacy rows default to "auto".
|
|
func IsValidCleanupPolicy(s string) bool {
|
|
switch s {
|
|
case "", CleanupPolicyAuto, CleanupPolicyAlways, CleanupPolicyNever:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NormalizeCleanupPolicy maps the empty string to CleanupPolicyAuto.
|
|
func NormalizeCleanupPolicy(s string) string {
|
|
if s == "" {
|
|
return CleanupPolicyAuto
|
|
}
|
|
return s
|
|
}
|
|
|
|
// MRWatch tracks active MR monitoring (session → MR). RepositoryID identifies
|
|
// which task repository the watched MR belongs to (multi-repo support; empty
|
|
// for single-repo tasks).
|
|
type MRWatch struct {
|
|
ID string `json:"id" db:"id"`
|
|
SessionID string `json:"session_id" db:"session_id"`
|
|
TaskID string `json:"task_id" db:"task_id"`
|
|
RepositoryID string `json:"repository_id,omitempty" db:"repository_id"`
|
|
ProjectPath string `json:"project_path" db:"project_path"`
|
|
MRIID int `json:"mr_iid" db:"mr_iid"`
|
|
Branch string `json:"branch" db:"branch"`
|
|
LastCheckedAt *time.Time `json:"last_checked_at,omitempty" db:"last_checked_at"`
|
|
LastNoteAt *time.Time `json:"last_note_at,omitempty" db:"last_note_at"`
|
|
LastPipelineState string `json:"last_pipeline_state" db:"last_pipeline_state"`
|
|
LastApprovalState string `json:"last_approval_state" db:"last_approval_state"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// ReviewWatch configures periodic polling for MRs needing the user's review.
|
|
// Projects holds the list of projects to monitor. An empty list means all
|
|
// projects the user has access to.
|
|
type ReviewWatch struct {
|
|
ID string `json:"id" db:"id"`
|
|
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
|
|
WorkflowID string `json:"workflow_id" db:"workflow_id"`
|
|
WorkflowStepID string `json:"workflow_step_id" db:"workflow_step_id"`
|
|
Projects []ProjectFilter `json:"projects" db:"-"`
|
|
ProjectsJSON string `json:"-" db:"projects"`
|
|
AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"`
|
|
ExecutorProfileID string `json:"executor_profile_id" db:"executor_profile_id"`
|
|
Prompt string `json:"prompt" db:"prompt"`
|
|
RepositoryID string `json:"repository_id,omitempty" db:"repository_id"`
|
|
BaseBranch string `json:"base_branch,omitempty" db:"base_branch"`
|
|
ReviewScope string `json:"review_scope" db:"review_scope"`
|
|
CustomQuery string `json:"custom_query" db:"custom_query"`
|
|
Enabled bool `json:"enabled" db:"enabled"`
|
|
PollIntervalSeconds int `json:"poll_interval_seconds" db:"poll_interval_seconds"`
|
|
CleanupPolicy string `json:"cleanup_policy" db:"cleanup_policy"`
|
|
MaxInflightTasks *int `json:"max_inflight_tasks,omitempty" db:"max_inflight_tasks"`
|
|
Generation int64 `json:"-" db:"generation"`
|
|
Deleting bool `json:"-" db:"deleting"`
|
|
LastError string `json:"last_error,omitempty" db:"last_error"`
|
|
LastErrorAt *time.Time `json:"last_error_at,omitempty" db:"last_error_at"`
|
|
LastPolledAt *time.Time `json:"last_polled_at,omitempty" db:"last_polled_at"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// ReviewMRTask records which MRs have already had tasks created (dedup).
|
|
type ReviewMRTask struct {
|
|
ID string `json:"id" db:"id"`
|
|
ReviewWatchID string `json:"review_watch_id" db:"review_watch_id"`
|
|
ProjectPath string `json:"project_path" db:"project_path"`
|
|
MRIID int `json:"mr_iid" db:"mr_iid"`
|
|
MRURL string `json:"mr_url" db:"mr_url"`
|
|
TaskID string `json:"task_id" db:"task_id"`
|
|
Generation int64 `json:"-" db:"generation"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
}
|
|
|
|
// IssueWatch configures periodic polling for GitLab issues matching a query.
|
|
type IssueWatch struct {
|
|
ID string `json:"id" db:"id"`
|
|
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
|
|
WorkflowID string `json:"workflow_id" db:"workflow_id"`
|
|
WorkflowStepID string `json:"workflow_step_id" db:"workflow_step_id"`
|
|
Projects []ProjectFilter `json:"projects" db:"-"`
|
|
ProjectsJSON string `json:"-" db:"projects"`
|
|
AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"`
|
|
ExecutorProfileID string `json:"executor_profile_id" db:"executor_profile_id"`
|
|
Prompt string `json:"prompt" db:"prompt"`
|
|
RepositoryID string `json:"repository_id,omitempty" db:"repository_id"`
|
|
BaseBranch string `json:"base_branch,omitempty" db:"base_branch"`
|
|
Labels []string `json:"labels" db:"-"`
|
|
LabelsJSON string `json:"-" db:"labels"`
|
|
CustomQuery string `json:"custom_query" db:"custom_query"`
|
|
Enabled bool `json:"enabled" db:"enabled"`
|
|
PollIntervalSeconds int `json:"poll_interval_seconds" db:"poll_interval_seconds"`
|
|
CleanupPolicy string `json:"cleanup_policy" db:"cleanup_policy"`
|
|
MaxInflightTasks *int `json:"max_inflight_tasks,omitempty" db:"max_inflight_tasks"`
|
|
Generation int64 `json:"-" db:"generation"`
|
|
Deleting bool `json:"-" db:"deleting"`
|
|
LastError string `json:"last_error,omitempty" db:"last_error"`
|
|
LastErrorAt *time.Time `json:"last_error_at,omitempty" db:"last_error_at"`
|
|
LastPolledAt *time.Time `json:"last_polled_at,omitempty" db:"last_polled_at"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// IssueWatchTask records which issues have had tasks created (dedup).
|
|
type IssueWatchTask struct {
|
|
ID string `json:"id" db:"id"`
|
|
IssueWatchID string `json:"issue_watch_id" db:"issue_watch_id"`
|
|
ProjectPath string `json:"project_path" db:"project_path"`
|
|
IssueIID int `json:"issue_iid" db:"issue_iid"`
|
|
IssueURL string `json:"issue_url" db:"issue_url"`
|
|
TaskID string `json:"task_id" db:"task_id"`
|
|
Generation int64 `json:"-" db:"generation"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
}
|
|
|
|
// CreateReviewWatchRequest is the request body for creating a review watch.
|
|
type CreateReviewWatchRequest struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
WorkflowID string `json:"workflow_id"`
|
|
WorkflowStepID string `json:"workflow_step_id"`
|
|
Projects []ProjectFilter `json:"projects"`
|
|
AgentProfileID string `json:"agent_profile_id"`
|
|
ExecutorProfileID string `json:"executor_profile_id"`
|
|
Prompt string `json:"prompt"`
|
|
RepositoryID string `json:"repository_id"`
|
|
BaseBranch string `json:"base_branch"`
|
|
ReviewScope string `json:"review_scope"`
|
|
CustomQuery string `json:"custom_query"`
|
|
PollIntervalSeconds int `json:"poll_interval_seconds"`
|
|
CleanupPolicy string `json:"cleanup_policy"`
|
|
MaxInflightTasks *int `json:"max_inflight_tasks,omitempty"`
|
|
}
|
|
|
|
// UpdateReviewWatchRequest is the request body for updating a review watch.
|
|
type UpdateReviewWatchRequest struct {
|
|
WorkflowID *string `json:"workflow_id,omitempty"`
|
|
WorkflowStepID *string `json:"workflow_step_id,omitempty"`
|
|
Projects *[]ProjectFilter `json:"projects,omitempty"`
|
|
AgentProfileID *string `json:"agent_profile_id,omitempty"`
|
|
ExecutorProfileID *string `json:"executor_profile_id,omitempty"`
|
|
Prompt *string `json:"prompt,omitempty"`
|
|
RepositoryID *string `json:"repository_id,omitempty"`
|
|
BaseBranch *string `json:"base_branch,omitempty"`
|
|
ReviewScope *string `json:"review_scope,omitempty"`
|
|
CustomQuery *string `json:"custom_query,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
PollIntervalSeconds *int `json:"poll_interval_seconds,omitempty"`
|
|
CleanupPolicy *string `json:"cleanup_policy,omitempty"`
|
|
// MaxInflightTasks updates the cap; zero explicitly clears it, while an
|
|
// omitted field leaves the existing cap unchanged.
|
|
MaxInflightTasks *int `json:"max_inflight_tasks,omitempty"`
|
|
}
|
|
|
|
// CreateIssueWatchRequest is the request body for creating an issue watch.
|
|
type CreateIssueWatchRequest struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
WorkflowID string `json:"workflow_id"`
|
|
WorkflowStepID string `json:"workflow_step_id"`
|
|
Projects []ProjectFilter `json:"projects"`
|
|
AgentProfileID string `json:"agent_profile_id"`
|
|
ExecutorProfileID string `json:"executor_profile_id"`
|
|
Prompt string `json:"prompt"`
|
|
RepositoryID string `json:"repository_id"`
|
|
BaseBranch string `json:"base_branch"`
|
|
Labels []string `json:"labels"`
|
|
CustomQuery string `json:"custom_query"`
|
|
PollIntervalSeconds int `json:"poll_interval_seconds"`
|
|
CleanupPolicy string `json:"cleanup_policy"`
|
|
MaxInflightTasks *int `json:"max_inflight_tasks,omitempty"`
|
|
}
|
|
|
|
// UpdateIssueWatchRequest is the request body for updating an issue watch.
|
|
type UpdateIssueWatchRequest struct {
|
|
WorkflowID *string `json:"workflow_id,omitempty"`
|
|
WorkflowStepID *string `json:"workflow_step_id,omitempty"`
|
|
Projects *[]ProjectFilter `json:"projects,omitempty"`
|
|
AgentProfileID *string `json:"agent_profile_id,omitempty"`
|
|
ExecutorProfileID *string `json:"executor_profile_id,omitempty"`
|
|
Prompt *string `json:"prompt,omitempty"`
|
|
RepositoryID *string `json:"repository_id,omitempty"`
|
|
BaseBranch *string `json:"base_branch,omitempty"`
|
|
Labels *[]string `json:"labels,omitempty"`
|
|
CustomQuery *string `json:"custom_query,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
PollIntervalSeconds *int `json:"poll_interval_seconds,omitempty"`
|
|
CleanupPolicy *string `json:"cleanup_policy,omitempty"`
|
|
// MaxInflightTasks updates the cap; zero explicitly clears it, while an
|
|
// omitted field leaves the existing cap unchanged.
|
|
MaxInflightTasks *int `json:"max_inflight_tasks,omitempty"`
|
|
}
|
|
|
|
// MRFeedbackEvent is published when an MR has new feedback (new discussion,
|
|
// pipeline state change, approval state change). Used by the frontend to
|
|
// surface a toast / refresh the task.
|
|
type MRFeedbackEvent struct {
|
|
SessionID string `json:"session_id"`
|
|
TaskID string `json:"task_id"`
|
|
ProjectPath string `json:"project_path"`
|
|
MRIID int `json:"mr_iid"`
|
|
NewPipelineState string `json:"new_pipeline_state"`
|
|
NewApprovalState string `json:"new_approval_state"`
|
|
}
|
|
|
|
// NewReviewMREvent is published when a new MR needing review is found by a
|
|
// review watch and a task has been auto-created.
|
|
type NewReviewMREvent struct {
|
|
ReviewWatchID string `json:"review_watch_id"`
|
|
WatchGeneration int64 `json:"watch_generation"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
WorkflowID string `json:"workflow_id"`
|
|
WorkflowStepID string `json:"workflow_step_id"`
|
|
AgentProfileID string `json:"agent_profile_id"`
|
|
ExecutorProfileID string `json:"executor_profile_id"`
|
|
Prompt string `json:"prompt"`
|
|
RepositoryID string `json:"repository_id,omitempty"`
|
|
BaseBranch string `json:"base_branch,omitempty"`
|
|
MaxInflightTasks *int `json:"max_inflight_tasks,omitempty"`
|
|
MR *MR `json:"mr"`
|
|
}
|
|
|
|
// NewIssueEvent is published when a new issue matching a watch is found.
|
|
type NewIssueEvent struct {
|
|
IssueWatchID string `json:"issue_watch_id"`
|
|
WatchGeneration int64 `json:"watch_generation"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
WorkflowID string `json:"workflow_id"`
|
|
WorkflowStepID string `json:"workflow_step_id"`
|
|
AgentProfileID string `json:"agent_profile_id"`
|
|
ExecutorProfileID string `json:"executor_profile_id"`
|
|
Prompt string `json:"prompt"`
|
|
RepositoryID string `json:"repository_id,omitempty"`
|
|
BaseBranch string `json:"base_branch,omitempty"`
|
|
MaxInflightTasks *int `json:"max_inflight_tasks,omitempty"`
|
|
Issue *Issue `json:"issue"`
|
|
}
|
|
|
|
// --- Stats ---
|
|
|
|
// Stats aggregates GitLab activity counts surfaced on the /gitlab page.
|
|
type Stats struct {
|
|
OpenMRs int `json:"open_mrs"`
|
|
MRsAwaitingMyReview int `json:"mrs_awaiting_my_review"`
|
|
OpenIssuesAssignedMe int `json:"open_issues_assigned_me"`
|
|
}
|
|
|
|
// --- Merge methods ---
|
|
|
|
// ProjectMergeMethods reports which merge methods a GitLab project allows.
|
|
// GitLab projects have a single `merge_method` setting; we expose it as a
|
|
// boolean set parallel to GitHub's allow_*_merge for the frontend.
|
|
type ProjectMergeMethods struct {
|
|
Merge bool `json:"merge"` // GitLab `merge_method=merge`
|
|
RebaseMerge bool `json:"rebase_merge"` // GitLab `merge_method=rebase_merge`
|
|
FastForward bool `json:"fast_forward"` // GitLab `merge_method=ff`
|
|
AllowSquash bool `json:"allow_squash"` // squash option on the merge request
|
|
}
|
|
|
|
// --- Action presets ---
|
|
|
|
// ActionPresetKind enumerates the two lists of quick-launch presets.
|
|
const (
|
|
ActionPresetKindMR = "mr"
|
|
ActionPresetKindIssue = "issue"
|
|
)
|
|
|
|
// ActionPreset is a single configurable quick-task launcher entry.
|
|
// PromptTemplate supports `{{url}}` and `{{title}}` placeholders substituted
|
|
// client-side when the dialog is opened.
|
|
type ActionPreset struct {
|
|
ID string `json:"id"`
|
|
Label string `json:"label"`
|
|
Hint string `json:"hint"`
|
|
Icon string `json:"icon"`
|
|
PromptTemplate string `json:"prompt_template"`
|
|
}
|
|
|
|
// ActionPresets groups the MR and Issue preset lists for a workspace.
|
|
type ActionPresets struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
MR []ActionPreset `json:"mr"`
|
|
Issue []ActionPreset `json:"issue"`
|
|
}
|
|
|
|
// UpdateActionPresetsRequest replaces one or both preset lists for a workspace.
|
|
// Nil fields leave the corresponding list unchanged.
|
|
type UpdateActionPresetsRequest struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
MR *[]ActionPreset `json:"mr,omitempty"`
|
|
Issue *[]ActionPreset `json:"issue,omitempty"`
|
|
}
|
|
|
|
// DefaultMRActionPresets returns the built-in MR presets used when a workspace
|
|
// has no stored overrides.
|
|
func DefaultMRActionPresets() []ActionPreset {
|
|
return []ActionPreset{
|
|
{
|
|
ID: "review",
|
|
Label: "Review",
|
|
Hint: "Read the diff, flag issues",
|
|
Icon: "eye",
|
|
PromptTemplate: "Review the merge request at {{url}}. Provide feedback on code quality, correctness, and suggest improvements.",
|
|
},
|
|
{
|
|
ID: "address_feedback",
|
|
Label: "Address feedback",
|
|
Hint: "Apply review comments",
|
|
Icon: "message",
|
|
PromptTemplate: "Review the feedback on the merge request at {{url}}. Evaluate each comment critically — apply changes that improve the code, push back on suggestions that are unnecessary or harmful, and explain your reasoning. Push the changes when done.",
|
|
},
|
|
{
|
|
ID: "fix_ci",
|
|
Label: "Fix pipeline",
|
|
Hint: "Diagnose failing jobs",
|
|
Icon: "tool",
|
|
PromptTemplate: "Investigate and fix the failing pipeline jobs and merge conflicts on the merge request at {{url}}. Run the failing jobs locally, resolve any conflicts, diagnose issues, and push fixes.",
|
|
},
|
|
}
|
|
}
|
|
|
|
// DefaultIssueActionPresets returns the built-in Issue presets used when a
|
|
// workspace has no stored overrides.
|
|
func DefaultIssueActionPresets() []ActionPreset {
|
|
return []ActionPreset{
|
|
{
|
|
ID: "implement",
|
|
Label: "Implement",
|
|
Hint: "Build and open an MR",
|
|
Icon: "code",
|
|
PromptTemplate: `Implement the changes described in the GitLab issue at {{url}} (title: "{{title}}"). Open a merge request when complete.`,
|
|
},
|
|
{
|
|
ID: "investigate",
|
|
Label: "Investigate",
|
|
Hint: "Find the root cause",
|
|
Icon: "search",
|
|
PromptTemplate: `Investigate the GitLab issue at {{url}} (title: "{{title}}"). Identify root cause and summarize findings.`,
|
|
},
|
|
{
|
|
ID: "reproduce",
|
|
Label: "Reproduce",
|
|
Hint: "Document repro steps",
|
|
Icon: "bug",
|
|
PromptTemplate: `Reproduce the bug described in the GitLab issue at {{url}} (title: "{{title}}"). Document the reproduction steps.`,
|
|
},
|
|
}
|
|
}
|