• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valksor / go-mehrhof / 20733530775

06 Jan 2026 12:24AM UTC coverage: 46.273% (-0.5%) from 46.751%
20733530775

push

github

k0d3r1s
Add configuration validation to GitHub provider

Integrate ValidateConfig into GitHub provider's New function for
configuration validation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Z.ai GLM-4.7 <noreply@z.ai>

6 of 8 new or added lines in 1 file covered. (75.0%)

752 existing lines in 19 files now uncovered.

14109 of 30491 relevant lines covered (46.27%)

5.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.5
/internal/provider/github/comments.go
1
package github
2

3
import (
4
        "context"
5
        "fmt"
6
        "regexp"
7
        "strconv"
8
        "strings"
9
        "time"
10

11
        "github.com/valksor/go-mehrhof/internal/provider"
12
        "github.com/valksor/go-mehrhof/internal/storage"
13
)
14

15
// FetchComments retrieves all comments from a GitHub issue.
16
func (p *Provider) FetchComments(ctx context.Context, workUnitID string) ([]provider.Comment, error) {
3✔
17
        ref, err := ParseReference(workUnitID)
3✔
18
        if err != nil {
4✔
19
                return nil, err
1✔
20
        }
1✔
21

22
        owner := ref.Owner
2✔
23
        repo := ref.Repo
2✔
24
        if owner == "" {
3✔
25
                owner = p.owner
1✔
26
        }
1✔
27
        if repo == "" {
3✔
28
                repo = p.repo
1✔
29
        }
1✔
30

31
        p.client.SetOwnerRepo(owner, repo)
2✔
32

2✔
33
        comments, err := p.client.GetIssueComments(ctx, ref.IssueNumber)
2✔
34
        if err != nil {
2✔
35
                return nil, err
×
36
        }
×
37

38
        result := make([]provider.Comment, len(comments))
2✔
39
        for i, c := range comments {
4✔
40
                result[i] = provider.Comment{
2✔
41
                        ID:        strconv.FormatInt(c.GetID(), 10),
2✔
42
                        Body:      c.GetBody(),
2✔
43
                        CreatedAt: c.GetCreatedAt().Time,
2✔
44
                        UpdatedAt: c.GetUpdatedAt().Time,
2✔
45
                        Author: provider.Person{
2✔
46
                                ID:   strconv.FormatInt(c.GetUser().GetID(), 10),
2✔
47
                                Name: c.GetUser().GetLogin(),
2✔
48
                        },
2✔
49
                }
2✔
50
        }
2✔
51

52
        return result, nil
2✔
53
}
54

55
// AddComment adds a comment to a GitHub issue.
56
func (p *Provider) AddComment(ctx context.Context, workUnitID string, body string) (*provider.Comment, error) {
3✔
57
        ref, err := ParseReference(workUnitID)
3✔
58
        if err != nil {
4✔
59
                return nil, err
1✔
60
        }
1✔
61

62
        owner := ref.Owner
2✔
63
        repo := ref.Repo
2✔
64
        if owner == "" {
3✔
65
                owner = p.owner
1✔
66
        }
1✔
67
        if repo == "" {
3✔
68
                repo = p.repo
1✔
69
        }
1✔
70

71
        p.client.SetOwnerRepo(owner, repo)
2✔
72

2✔
73
        comment, err := p.client.AddComment(ctx, ref.IssueNumber, body)
2✔
74
        if err != nil {
2✔
75
                return nil, err
×
UNCOV
76
        }
×
77

78
        return &provider.Comment{
2✔
79
                ID:        strconv.FormatInt(comment.GetID(), 10),
2✔
80
                Body:      comment.GetBody(),
2✔
81
                CreatedAt: comment.GetCreatedAt().Time,
2✔
82
                Author: provider.Person{
2✔
83
                        ID:   strconv.FormatInt(comment.GetUser().GetID(), 10),
2✔
84
                        Name: comment.GetUser().GetLogin(),
2✔
85
                },
2✔
86
        }, nil
2✔
87
}
88

89
// CommentGenerator generates comment content for various events.
90
type CommentGenerator struct {
91
        provider *Provider
92
}
93

94
// NewCommentGenerator creates a new comment generator.
95
func NewCommentGenerator(p *Provider) *CommentGenerator {
1✔
96
        return &CommentGenerator{provider: p}
1✔
97
}
1✔
98

99
// GenerateBranchCreatedComment generates comment for branch creation.
100
func (g *CommentGenerator) GenerateBranchCreatedComment(branchName string) string {
2✔
101
        return fmt.Sprintf("Started working on this issue.\nBranch: `%s`", branchName)
2✔
102
}
2✔
103

104
// GeneratePlanComment generates comment summarizing the implementation plan
105
// This extracts key information from specification files.
106
func (g *CommentGenerator) GeneratePlanComment(specs []*storage.Specification) string {
5✔
107
        if len(specs) == 0 {
7✔
108
                return "Planning complete."
2✔
109
        }
2✔
110

111
        var sb strings.Builder
3✔
112
        sb.WriteString("## Implementation Plan\n\n")
3✔
113

3✔
114
        // Use the latest specification
3✔
115
        spec := specs[len(specs)-1]
3✔
116

3✔
117
        // Extract planned files from specification content
3✔
118
        plannedFiles := extractPlannedFiles(spec.Content)
3✔
119
        if len(plannedFiles) > 0 {
5✔
120
                sb.WriteString("**Files to be created/modified:**\n")
2✔
121
                for _, f := range plannedFiles {
5✔
122
                        sb.WriteString(fmt.Sprintf("- `%s`\n", f))
3✔
123
                }
3✔
124
                sb.WriteString("\n")
2✔
125
        }
126

127
        // Extract approach summary (first paragraph after "## Approach" or similar)
128
        approach := extractApproachSummary(spec.Content)
3✔
129
        if approach != "" {
4✔
130
                sb.WriteString("**Approach:**\n")
1✔
131
                sb.WriteString(approach)
1✔
132
                sb.WriteString("\n")
1✔
133
        }
1✔
134

135
        return sb.String()
3✔
136
}
137

138
// GenerateImplementComment generates comment summarizing implementation changes.
139
func (g *CommentGenerator) GenerateImplementComment(diffStat string, summary string) string {
4✔
140
        var sb strings.Builder
4✔
141
        sb.WriteString("## Implementation Complete\n\n")
4✔
142

4✔
143
        if summary != "" {
6✔
144
                sb.WriteString("**Summary:**\n")
2✔
145
                sb.WriteString(summary)
2✔
146
                sb.WriteString("\n\n")
2✔
147
        }
2✔
148

149
        if diffStat != "" {
6✔
150
                sb.WriteString("**Files changed:**\n```\n")
2✔
151
                sb.WriteString(diffStat)
2✔
152
                sb.WriteString("\n```\n\n")
2✔
153
        }
2✔
154

155
        sb.WriteString("Ready for review.")
4✔
156

4✔
157
        return sb.String()
4✔
158
}
159

160
// GeneratePRCreatedComment generates comment for PR creation.
161
func (g *CommentGenerator) GeneratePRCreatedComment(prNumber int, prURL string) string {
1✔
162
        return fmt.Sprintf("Pull request created: #%d\n%s", prNumber, prURL)
1✔
163
}
1✔
164

165
// --- Helper functions for extracting info from specifications ---
166

167
// extractPlannedFiles finds file paths mentioned in specification content.
168
func extractPlannedFiles(content string) []string {
11✔
169
        var files []string
11✔
170
        seen := make(map[string]bool)
11✔
171

11✔
172
        // Match various patterns for file references
11✔
173
        patterns := []*regexp.Regexp{
11✔
174
                regexp.MustCompile("`([a-zA-Z0-9_/.-]+\\.[a-z]+)`"),                          // `path/to/file.ext`
11✔
175
                regexp.MustCompile("(?:create|modify|update|add|edit).*?`([^`]+\\.[a-z]+)`"), // create/modify `file.ext`
11✔
176
                regexp.MustCompile("- `([^`]+\\.[a-z]+)`"),                                   // - `file.ext` (list items)
11✔
177
        }
11✔
178

11✔
179
        for _, pattern := range patterns {
44✔
180
                matches := pattern.FindAllStringSubmatch(content, -1)
33✔
181
                for _, m := range matches {
53✔
182
                        if len(m) > 1 {
40✔
183
                                path := m[1]
20✔
184
                                // Filter out obvious non-file patterns
20✔
185
                                if isLikelyFilePath(path) && !seen[path] {
32✔
186
                                        seen[path] = true
12✔
187
                                        files = append(files, path)
12✔
188
                                }
12✔
189
                        }
190
                }
191
        }
192

193
        return files
11✔
194
}
195

196
// isLikelyFilePath checks if a string looks like a file path.
197
func isLikelyFilePath(s string) bool {
58✔
198
        // Must have an extension
58✔
199
        if !strings.Contains(s, ".") {
60✔
200
                return false
2✔
201
        }
2✔
202

203
        // Common code file extensions
204
        validExts := []string{
56✔
205
                ".go", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".kt", ".swift",
56✔
206
                ".rs", ".rb", ".php", ".c", ".cpp", ".h", ".hpp", ".cs", ".m", ".mm",
56✔
207
                ".md", ".yaml", ".yml", ".json", ".toml", ".xml", ".html", ".css",
56✔
208
                ".sql", ".sh", ".bash", ".zsh", ".ps1", ".bat",
56✔
209
        }
56✔
210

56✔
211
        for _, ext := range validExts {
766✔
212
                if strings.HasSuffix(strings.ToLower(s), ext) {
762✔
213
                        return true
52✔
214
                }
52✔
215
        }
216

217
        return false
4✔
218
}
219

220
// extractApproachSummary extracts approach/strategy summary from specification.
221
func extractApproachSummary(content string) string {
10✔
222
        // Look for common approach headings
10✔
223
        headingPatterns := []string{
10✔
224
                `(?i)##\s*approach\s*\n+([^\n#]+)`,
10✔
225
                `(?i)##\s*strategy\s*\n+([^\n#]+)`,
10✔
226
                `(?i)##\s*implementation\s+approach\s*\n+([^\n#]+)`,
10✔
227
                `(?i)##\s*solution\s*\n+([^\n#]+)`,
10✔
228
        }
10✔
229

10✔
230
        for _, pattern := range headingPatterns {
38✔
231
                re := regexp.MustCompile(pattern)
28✔
232
                match := re.FindStringSubmatch(content)
28✔
233
                if len(match) > 1 {
34✔
234
                        return strings.TrimSpace(match[1])
6✔
235
                }
6✔
236
        }
237

238
        return ""
4✔
239
}
240

241
// ParseDiffStat parses git diff --stat output and formats it nicely.
242
func ParseDiffStat(diffOutput string) string {
6✔
243
        lines := strings.Split(strings.TrimSpace(diffOutput), "\n")
6✔
244
        if len(lines) == 0 {
6✔
245
                return ""
×
UNCOV
246
        }
×
247

248
        var sb strings.Builder
6✔
249
        for _, line := range lines {
14✔
250
                line = strings.TrimSpace(line)
8✔
251
                if line == "" {
10✔
252
                        continue
2✔
253
                }
254
                // Format: file | count ++++----
255
                sb.WriteString(line)
6✔
256
                sb.WriteString("\n")
6✔
257
        }
258

259
        return sb.String()
6✔
260
}
261

262
// GenerateChangeSummary creates a brief summary of changes from session exchanges.
263
func GenerateChangeSummary(exchanges []storage.Exchange) string {
5✔
264
        var changedFiles []string
5✔
265
        fileOps := make(map[string]string)
5✔
266

5✔
267
        for _, ex := range exchanges {
11✔
268
                for _, fc := range ex.FilesChanged {
12✔
269
                        if _, exists := fileOps[fc.Path]; !exists {
11✔
270
                                fileOps[fc.Path] = fc.Operation
5✔
271
                                changedFiles = append(changedFiles, fc.Path)
5✔
272
                        }
5✔
273
                }
274
        }
275

276
        if len(changedFiles) == 0 {
7✔
277
                return ""
2✔
278
        }
2✔
279

280
        var sb strings.Builder
3✔
281
        for _, f := range changedFiles {
8✔
282
                op := fileOps[f]
5✔
283
                sb.WriteString(fmt.Sprintf("- `%s` (%s)\n", f, op))
5✔
284
        }
5✔
285

286
        return sb.String()
3✔
287
}
288

289
// CommentEvent represents a comment event for the comment service.
290
type CommentEvent string
291

292
const (
293
        CommentEventBranchCreated CommentEvent = "branch_created"
294
        CommentEventPlanDone      CommentEvent = "plan_done"
295
        CommentEventImplementDone CommentEvent = "implement_done"
296
        CommentEventPRCreated     CommentEvent = "pr_created"
297
)
298

299
// ShouldComment checks if a comment should be posted for the given event.
300
func (p *Provider) ShouldComment(event CommentEvent) bool {
×
301
        if p.config == nil || p.config.Comments == nil || !p.config.Comments.Enabled {
×
302
                return false
×
UNCOV
303
        }
×
304

305
        switch event {
×
306
        case CommentEventBranchCreated:
×
307
                return p.config.Comments.OnBranchCreated
×
308
        case CommentEventPlanDone:
×
309
                return p.config.Comments.OnPlanDone
×
310
        case CommentEventImplementDone:
×
311
                return p.config.Comments.OnImplementDone
×
312
        case CommentEventPRCreated:
×
313
                return p.config.Comments.OnPRCreated
×
314
        default:
×
UNCOV
315
                return false
×
316
        }
317
}
318

319
// PostCommentIfEnabled posts a comment if enabled for the given event.
320
func (p *Provider) PostCommentIfEnabled(ctx context.Context, workUnitID string, event CommentEvent, body string) error {
×
321
        if !p.ShouldComment(event) {
×
322
                return nil
×
UNCOV
323
        }
×
324

325
        _, err := p.AddComment(ctx, workUnitID, body)
×
326

×
UNCOV
327
        return err
×
328
}
329

330
// CommentTimestamp returns formatted timestamp for comment.
331
func CommentTimestamp() string {
1✔
332
        return time.Now().Format("2006-01-02 15:04:05 UTC")
1✔
333
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc