-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathgitlab.go
More file actions
304 lines (254 loc) · 9.88 KB
/
gitlab.go
File metadata and controls
304 lines (254 loc) · 9.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
package gitlab
import (
"context"
"fmt"
"log"
"net/url"
"regexp"
"strconv"
"github.com/livereview/internal/aisanitize"
gitlab "gitlab.com/gitlab-org/api/client-go"
"github.com/livereview/internal/providers"
"github.com/livereview/pkg/models"
)
// GitLabProvider implements the Provider interface for GitLab
type GitLabProvider struct {
client *gitlab.Client
httpClient *GitLabHTTPClient
config GitLabConfig
projectID string // Store the current project ID
currentMRID int // Store the current MR IID
}
// GitLabConfig contains configuration for the GitLab provider
type GitLabConfig struct {
URL string `koanf:"url"`
Token string `koanf:"token"`
}
// New creates a new GitLabProvider
func New(config GitLabConfig) (*GitLabProvider, error) {
// Create a new client with nil HTTP client (defaults to http.DefaultClient)
// and the provided token
client := gitlab.NewClient(nil, config.Token)
// Set the base URL for the GitLab API
if config.URL != "" {
err := client.SetBaseURL(fmt.Sprintf("%s/api/v4", config.URL))
if err != nil {
return nil, fmt.Errorf("failed to set GitLab API base URL: %w", err)
}
}
// Initialize our custom HTTP client that bypasses the endpoint issues
httpClient := NewHTTPClient(config.URL, config.Token)
fmt.Printf("Initialized GitLab client with URL: %s\n", config.URL)
return &GitLabProvider{
client: client,
httpClient: httpClient,
config: config,
}, nil
}
// GetMergeRequestDetails retrieves the details of a merge request
func (p *GitLabProvider) GetMergeRequestDetails(ctx context.Context, mrURL string) (*providers.MergeRequestDetails, error) {
// Extract project ID and MR IID from URL
projectID, mrIID, err := p.extractMRInfo(mrURL)
if err != nil {
return nil, err
}
// Store the project ID and MR IID for later use
p.projectID = projectID
p.currentMRID = mrIID
// Make an API call to get the merge request details using our custom HTTP client
fmt.Printf("Fetching GitLab MR details for project=%s, mrIID=%d using custom HTTP client\n", projectID, mrIID)
mr, err := p.httpClient.GetMergeRequest(projectID, mrIID)
if err != nil {
return nil, fmt.Errorf("failed to fetch merge request: %w", err)
}
// Convert GitLab MR to our internal model
return ConvertToMergeRequestDetails(mr, projectID), nil
}
// GetMergeRequestChanges retrieves the code changes in a merge request
func (p *GitLabProvider) GetMergeRequestChanges(ctx context.Context, mrID string) ([]*models.CodeDiff, error) {
// Convert mrID to integer
mrIID, err := strconv.Atoi(mrID)
if err != nil {
return nil, fmt.Errorf("invalid MR ID: %w", err)
}
// Use the stored project ID if available, otherwise try to extract from URL
projectID := p.projectID
if projectID == "" {
// Fallback to trying to extract from a URL (this might not work)
var extractErr error
projectID, _, extractErr = p.extractMRInfo(fmt.Sprintf("%s/-/merge_requests/%d", p.config.URL, mrIID))
if extractErr != nil {
return nil, fmt.Errorf("failed to get project ID: %w", extractErr)
}
}
// Get merge request changes using our custom HTTP client
fmt.Printf("Fetching GitLab MR changes for project=%s, mrIID=%d using custom HTTP client\n", projectID, mrIID)
changes, err := p.httpClient.GetMergeRequestChanges(projectID, mrIID)
if err != nil {
return nil, fmt.Errorf("failed to fetch merge request changes: %w", err)
}
// Convert the changes to our internal model
return ConvertToCodeDiffs(changes), nil
}
// GetMergeRequestChangesAsText retrieves the code changes in a merge request as a raw text diff
func (p *GitLabProvider) GetMergeRequestChangesAsText(ctx context.Context, mrID string) (string, error) {
// Convert mrID to integer
mrIID, err := strconv.Atoi(mrID)
if err != nil {
return "", fmt.Errorf("invalid MR ID: %w", err)
}
// Use the stored project ID if available, otherwise try to extract from URL
projectID := p.projectID
if projectID == "" {
// Fallback to trying to extract from a URL (this might not work)
var extractErr error
projectID, _, extractErr = p.extractMRInfo(fmt.Sprintf("%s/-/merge_requests/%d", p.config.URL, mrIID))
if extractErr != nil {
return "", fmt.Errorf("failed to get project ID: %w", extractErr)
}
}
// Get merge request changes using our custom HTTP client
fmt.Printf("Fetching GitLab MR changes for project=%s, mrIID=%d using custom HTTP client\n", projectID, mrIID)
changes, err := p.httpClient.GetMergeRequestChangesRaw(projectID, mrIID)
if err != nil {
return "", fmt.Errorf("failed to fetch merge request changes: %w", err)
}
return changes, nil
}
// PostComment posts a comment on a merge request
func (p *GitLabProvider) PostComment(ctx context.Context, mrID string, comment *models.ReviewComment) error {
// Convert mrID to integer
mrIID, err := strconv.Atoi(mrID)
if err != nil {
return fmt.Errorf("invalid MR ID: %w", err)
}
// Use the stored project ID if available, otherwise try to extract from URL
projectID := p.projectID
if projectID == "" {
// Fallback to trying to extract from a URL (this might not work)
var extractErr error
projectID, _, extractErr = p.extractMRInfo(fmt.Sprintf("%s/-/merge_requests/%d", p.config.URL, mrIID))
if extractErr != nil {
return fmt.Errorf("failed to get project ID: %w", extractErr)
}
}
// Format the comment for GitLab, ensuring consistent output with no duplications
commentText := formatGitLabComment(comment)
fmt.Printf("Posting comment on MR #%d for project %s\n", mrIID, projectID)
// If we have file path and line number, post a line-specific comment
if comment.FilePath != "" && comment.Line > 0 {
lineType := "new_line"
if comment.IsDeletedLine {
lineType = "old_line"
}
fmt.Printf("\nGITLAB PROVIDER [%s:%d]: Posting comment as line comment (isDeletedLine=%v, type: %s)\n",
comment.FilePath, comment.Line, comment.IsDeletedLine, lineType)
return p.httpClient.CreateMRLineComment(projectID, mrIID, comment.FilePath, comment.Line, commentText, comment.IsDeletedLine)
} else {
// Otherwise post a general MR comment
fmt.Printf("\nGITLAB PROVIDER: Posting general MR comment (no file/line specified)\n")
return p.httpClient.CreateMRGeneralComment(projectID, mrIID, commentText)
}
}
// PostComments posts multiple comments on a merge request
func (p *GitLabProvider) PostComments(ctx context.Context, mrID string, comments []*models.ReviewComment) error {
for _, comment := range comments {
if err := p.PostComment(ctx, mrID, comment); err != nil {
return err
}
}
return nil
}
// Name returns the name of the provider
func (p *GitLabProvider) Name() string {
return "gitlab"
}
// GetHTTPClient returns the HTTP client for direct access in tests
func (p *GitLabProvider) GetHTTPClient() *GitLabHTTPClient {
return p.httpClient
}
// Configure configures the provider with the given configuration
func (p *GitLabProvider) Configure(config map[string]interface{}) error {
// Extract URL
if url, ok := config["url"].(string); ok && url != "" {
p.config.URL = url
} else {
return fmt.Errorf("GitLab URL is required")
}
// Extract token
if token, ok := config["token"].(string); ok && token != "" {
p.config.Token = token
} else {
return fmt.Errorf("GitLab token is required")
}
// Create a new client with the provided token
client := gitlab.NewClient(nil, p.config.Token)
// Set the base URL for the GitLab API
err := client.SetBaseURL(fmt.Sprintf("%s/api/v4", p.config.URL))
if err != nil {
return fmt.Errorf("failed to set GitLab API base URL: %w", err)
}
// Initialize our custom HTTP client
httpClient := NewHTTPClient(p.config.URL, p.config.Token)
// Update the provider
p.client = client
p.httpClient = httpClient
return nil
}
// extractMRInfo extracts project ID and MR IID from a GitLab MR URL
func (p *GitLabProvider) extractMRInfo(mrURL string) (string, int, error) {
// Parse URL to extract project and MR IID
// Example URL: https://gitlab.example.com/group/project/-/merge_requests/123
parsedURL, err := url.Parse(mrURL)
if err != nil {
return "", 0, fmt.Errorf("invalid URL: %w", err)
}
// Remove the leading slash from the path
path := parsedURL.Path
if path[0] == '/' {
path = path[1:]
}
// Look for the merge_requests part
re := regexp.MustCompile(`(.+)/-/merge_requests/(\d+)$`)
matches := re.FindStringSubmatch(path)
if len(matches) != 3 {
return "", 0, fmt.Errorf("could not extract project and MR ID from URL: %s", mrURL)
}
projectPath := matches[1]
mrIID, err := strconv.Atoi(matches[2])
if err != nil {
return "", 0, fmt.Errorf("invalid MR ID: %w", err)
}
fmt.Printf("Extracted project=%s, mrIID=%d from URL=%s\n", projectPath, mrIID, mrURL)
return projectPath, mrIID, nil
}
// formatGitLabComment creates a consistently formatted comment for GitLab
// with severity information and suggestions properly formatted
func formatGitLabComment(comment *models.ReviewComment) string {
safeContent, contentReport := aisanitize.SanitizationPostflight(context.Background(), comment.Content)
if contentReport.PIIRedactError {
log.Printf("[WARN] GitLab comment sanitization reported internal error for content")
}
safeSuggestions := make([]string, 0, len(comment.Suggestions))
for _, suggestion := range comment.Suggestions {
safeSuggestion, suggestionReport := aisanitize.SanitizationPostflight(context.Background(), suggestion)
if suggestionReport.PIIRedactError {
log.Printf("[WARN] GitLab comment sanitization reported internal error for suggestion")
}
safeSuggestions = append(safeSuggestions, safeSuggestion)
}
// Start with the content
formattedComment := safeContent
// Add severity information at the beginning
if comment.Severity != "" {
formattedComment = fmt.Sprintf("**Severity: %s**\n\n%s", comment.Severity, formattedComment)
}
// Add suggestions section if we have any
if len(safeSuggestions) > 0 {
formattedComment += "\n\n**Suggestions:**\n"
for i, suggestion := range safeSuggestions {
formattedComment += fmt.Sprintf("%d. %s\n", i+1, suggestion)
}
}
return formattedComment
}