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

valksor / go-mehrhof / 21342980857

26 Jan 2026 01:09AM UTC coverage: 40.055% (-0.5%) from 40.54%
21342980857

push

github

k0d3r1s
Update provider packages to use go-toolkit slug directly

Replaces internal/naming.Slugify wrapper with go-toolkit/slug.Slugify
across all provider packages. Updates mock event bus to use eventbus.

Changes:
- Update provider imports: remove internal/naming, add github.com/valksor/go-toolkit/slug
- Replace naming.Slugify() calls with slug.Slugify()
- Update mock_eventbus.go to use go-toolkit/eventbus types
- Rename local 'slug' variables to avoid shadowing package name

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

7 of 61 new or added lines in 31 files covered. (11.48%)

402 existing lines in 15 files now uncovered.

21307 of 53194 relevant lines covered (40.06%)

21.46 hits per line

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

48.65
/internal/provider/gitlab/gitlab.go
1
package gitlab
2

3
import (
4
        "context"
5
        "crypto/sha256"
6
        "encoding/hex"
7
        "fmt"
8
        "strconv"
9
        "strings"
10
        "time"
11

12
        gl "gitlab.com/gitlab-org/api/client-go"
13

14
        "github.com/valksor/go-mehrhof/internal/provider"
15
        "github.com/valksor/go-toolkit/slug"
16
)
17

18
// hashURL generates a stable 8-character hash from a URL for attachment IDs.
19
// This ensures attachment IDs remain stable even if images are reordered.
20
func hashURL(url string) string {
8✔
21
        h := sha256.Sum256([]byte(url))
8✔
22

8✔
23
        return hex.EncodeToString(h[:4])
8✔
24
}
8✔
25

26
// ProviderName is the registered name for this provider.
27
const ProviderName = "gitlab"
28

29
// Provider handles GitLab issue tasks.
30
type Provider struct {
31
        client      *Client
32
        projectPath string
33
        config      *Config
34
}
35

36
// Config holds GitLab provider configuration.
37
type Config struct {
38
        Token              string
39
        Host               string // e.g., "https://gitlab.com" or custom host
40
        ProjectPath        string // e.g., "group/project" or "12345" (project ID)
41
        ProjectID          int64  // Numeric project ID (alternative to path)
42
        BranchPattern      string // Default: "issue/{key}-{slug}"
43
        CommitPrefix       string // Default: "[#{key}]"
44
        TargetBranch       string // Target branch for MRs (default: repo default branch)
45
        DraftMR            bool   // Create MRs as draft by default
46
        RemoveSourceBranch bool   // Remove source branch when MR is merged
47
}
48

49
// Info returns provider metadata.
50
func Info() provider.ProviderInfo {
3✔
51
        return provider.ProviderInfo{
3✔
52
                Name:        ProviderName,
3✔
53
                Description: "GitLab Issues task source",
3✔
54
                Schemes:     []string{"gitlab", "gl"},
3✔
55
                Priority:    20, // Higher than file/directory
3✔
56
                Capabilities: provider.CapabilitySet{
3✔
57
                        provider.CapRead:               true,
3✔
58
                        provider.CapList:               true,
3✔
59
                        provider.CapFetchComments:      true,
3✔
60
                        provider.CapComment:            true,
3✔
61
                        provider.CapUpdateStatus:       true,
3✔
62
                        provider.CapManageLabels:       true,
3✔
63
                        provider.CapCreateWorkUnit:     true,
3✔
64
                        provider.CapDownloadAttachment: true,
3✔
65
                        provider.CapSnapshot:           true,
3✔
66
                        provider.CapCreatePR:           true, // MR creation
3✔
67
                        provider.CapFetchSubtasks:      true,
3✔
68
                        provider.CapFetchPR:            true,
3✔
69
                        provider.CapPRComment:          true,
3✔
70
                        provider.CapFetchPRComments:    true,
3✔
71
                        provider.CapUpdatePRComment:    true,
3✔
72
                        provider.CapCreateDependency:   true,
3✔
73
                        provider.CapFetchDependencies:  true,
3✔
74
                },
3✔
75
        }
3✔
76
}
3✔
77

78
// New creates a GitLab provider.
79
func New(ctx context.Context, cfg provider.Config) (any, error) {
4✔
80
        // Extract config values
4✔
81
        token := cfg.GetString("token")
4✔
82
        host := cfg.GetString("host")
4✔
83
        projectPath := cfg.GetString("project_path")
4✔
84

4✔
85
        // Resolve token
4✔
86
        resolvedToken, err := ResolveToken(token)
4✔
87
        if err != nil {
5✔
88
                return nil, err
1✔
89
        }
1✔
90

91
        // Set defaults
92
        if host == "" {
4✔
93
                host = "https://gitlab.com"
1✔
94
        }
1✔
95
        // Strip trailing slash from host
96
        host = strings.TrimSuffix(host, "/")
3✔
97

3✔
98
        // Set defaults for branch/commit patterns
3✔
99
        branchPattern := cfg.GetString("branch_pattern")
3✔
100
        if branchPattern == "" {
5✔
101
                branchPattern = "issue/{key}-{slug}"
2✔
102
        }
2✔
103
        commitPrefix := cfg.GetString("commit_prefix")
3✔
104
        if commitPrefix == "" {
5✔
105
                commitPrefix = "[#{key}]"
2✔
106
        }
2✔
107

108
        // MR-related config
109
        targetBranch := cfg.GetString("target_branch")
3✔
110
        draftMR := cfg.GetBool("draft_mr")
3✔
111
        removeSourceBranch := cfg.GetBool("remove_source_branch")
3✔
112

3✔
113
        config := &Config{
3✔
114
                Token:              resolvedToken,
3✔
115
                Host:               host,
3✔
116
                ProjectPath:        projectPath,
3✔
117
                BranchPattern:      branchPattern,
3✔
118
                CommitPrefix:       commitPrefix,
3✔
119
                TargetBranch:       targetBranch,
3✔
120
                DraftMR:            draftMR,
3✔
121
                RemoveSourceBranch: removeSourceBranch,
3✔
122
        }
3✔
123

3✔
124
        return &Provider{
3✔
125
                client: NewClient(resolvedToken, host, projectPath, 0),
3✔
126
                config: config,
3✔
127
        }, nil
3✔
128
}
129

130
// Match checks if input has the gitlab: or gl: scheme prefix.
131
func (p *Provider) Match(input string) bool {
6✔
132
        return strings.HasPrefix(input, "gitlab:") || strings.HasPrefix(input, "gl:")
6✔
133
}
6✔
134

135
// Parse extracts the issue reference from input.
136
func (p *Provider) Parse(input string) (string, error) {
5✔
137
        ref, err := ParseReference(input)
5✔
138
        if err != nil {
5✔
139
                return "", err
×
140
        }
×
141

142
        // If explicit project provided, use it
143
        if ref.IsExplicit {
7✔
144
                if ref.ProjectPath != "" {
3✔
145
                        return fmt.Sprintf("%s#%d", ref.ProjectPath, ref.IssueIID), nil
1✔
146
                }
1✔
147
                if ref.ProjectID > 0 {
2✔
148
                        return fmt.Sprintf("%d#%d", ref.ProjectID, ref.IssueIID), nil
1✔
149
                }
1✔
150
        }
151

152
        // Otherwise, check if we have project configured
153
        projectPath := p.projectPath
3✔
154
        if projectPath == "" {
4✔
155
                projectPath = p.config.ProjectPath
1✔
156
        }
1✔
157

158
        if projectPath == "" {
4✔
159
                return "", fmt.Errorf("%w: use gitlab:group/project#N format or configure gitlab.project_path", ErrProjectNotConfigured)
1✔
160
        }
1✔
161

162
        return fmt.Sprintf("%s#%d", projectPath, ref.IssueIID), nil
2✔
163
}
164

165
// Fetch reads a GitLab issue and creates a WorkUnit.
166
func (p *Provider) Fetch(ctx context.Context, id string) (*provider.WorkUnit, error) {
×
167
        ref, err := ParseReference(id)
×
168
        if err != nil {
×
169
                return nil, err
×
170
        }
×
171

172
        // Determine project
173
        projectPath := ref.ProjectPath
×
174
        if projectPath == "" {
×
175
                projectPath = p.projectPath
×
176
                if projectPath == "" {
×
177
                        projectPath = p.config.ProjectPath
×
178
                }
×
179
        }
180

181
        // If explicit project ID in reference, use it
182
        if ref.ProjectID > 0 {
×
183
                p.client.SetProjectID(ref.ProjectID)
×
184
        } else if projectPath != "" {
×
185
                p.client.SetProjectPath(projectPath)
×
186
        } else {
×
187
                return nil, ErrProjectNotConfigured
×
188
        }
×
189

190
        // Fetch issue
191
        issue, err := p.client.GetIssue(ctx, ref.IssueIID)
×
192
        if err != nil {
×
193
                return nil, err
×
194
        }
×
195

196
        // Determine the project path for ExternalID
197
        displayProject := projectPath
×
198
        if displayProject == "" && ref.ProjectID > 0 {
×
199
                displayProject = strconv.FormatInt(ref.ProjectID, 10)
×
200
        } else if displayProject == "" && issue.ProjectID != 0 {
×
201
                displayProject = strconv.FormatInt(issue.ProjectID, 10)
×
202
        }
×
203

204
        // Map to WorkUnit
205
        wu := &provider.WorkUnit{
×
206
                ID:          strconv.FormatInt(issue.IID, 10),
×
207
                ExternalID:  fmt.Sprintf("%s#%d", displayProject, issue.IID),
×
208
                Provider:    ProviderName,
×
209
                Title:       issue.Title,
×
210
                Description: issue.Description,
×
211
                Status:      mapGitLabState(issue.State),
×
212
                Priority:    inferPriorityFromLabels(issue.Labels),
×
213
                Labels:      issue.Labels,
×
214
                Assignees:   mapAssignees(issue.Assignees),
×
215
                CreatedAt:   *issue.CreatedAt,
×
216
                UpdatedAt:   *issue.UpdatedAt,
×
217
                Source: provider.SourceInfo{
×
218
                        Type:      ProviderName,
×
219
                        Reference: id,
×
220
                        SyncedAt:  time.Now(),
×
221
                },
×
222

×
223
                // Naming fields for branch/commit customization
×
224
                ExternalKey: strconv.FormatInt(issue.IID, 10),
×
225
                TaskType:    inferTypeFromLabels(issue.Labels),
×
NEW
226
                Slug:        slug.Slugify(issue.Title, 50),
×
227

×
228
                Metadata: map[string]any{
×
229
                        "web_url":        issue.WebURL,
×
230
                        "project_path":   displayProject,
×
231
                        "project_id":     issue.ProjectID,
×
232
                        "issue_iid":      issue.IID,
×
233
                        "branch_pattern": p.config.BranchPattern,
×
234
                        "commit_prefix":  p.config.CommitPrefix,
×
235
                        "host":           p.client.Host(),
×
236
                },
×
237
        }
×
238

×
239
        // Fetch notes (comments) if available
×
240
        notes, err := p.client.GetIssueNotes(ctx, ref.IssueIID)
×
241
        if err == nil && len(notes) > 0 {
×
242
                wu.Comments = mapNotes(notes)
×
243
        }
×
244

245
        // Extract linked issues
246
        linkedIIDs := ExtractLinkedIssues(issue.Description)
×
247
        if len(linkedIIDs) > 0 {
×
248
                wu.Metadata["linked_issues"] = linkedIIDs
×
249
        }
×
250

251
        // Extract image URLs
252
        imageURLs := ExtractImageURLs(issue.Description)
×
253
        if len(imageURLs) > 0 {
×
254
                wu.Attachments = make([]provider.Attachment, len(imageURLs))
×
255
                for i, url := range imageURLs {
×
256
                        wu.Attachments[i] = provider.Attachment{
×
257
                                ID:   "img-" + hashURL(url), // Hash-based ID for stability
×
258
                                Name: fmt.Sprintf("image-%d", i),
×
259
                                URL:  url,
×
260
                        }
×
261
                }
×
262
        }
263

264
        return wu, nil
×
265
}
266

267
// Snapshot captures the issue content for storage.
268
func (p *Provider) Snapshot(ctx context.Context, id string) (*provider.Snapshot, error) {
×
269
        ref, err := ParseReference(id)
×
270
        if err != nil {
×
271
                return nil, err
×
272
        }
×
273

274
        // Determine project
275
        projectPath := ref.ProjectPath
×
276
        if projectPath == "" {
×
277
                projectPath = p.projectPath
×
278
                if projectPath == "" {
×
279
                        projectPath = p.config.ProjectPath
×
280
                }
×
281
        }
282

283
        if ref.ProjectID > 0 {
×
284
                p.client.SetProjectID(ref.ProjectID)
×
285
        } else if projectPath != "" {
×
286
                p.client.SetProjectPath(projectPath)
×
287
        } else {
×
288
                return nil, ErrProjectNotConfigured
×
289
        }
×
290

291
        // Fetch main issue
292
        issue, err := p.client.GetIssue(ctx, ref.IssueIID)
×
293
        if err != nil {
×
294
                return nil, err
×
295
        }
×
296

297
        snapshot := &provider.Snapshot{
×
298
                Type: ProviderName,
×
299
                Ref:  id,
×
300
                Files: []provider.SnapshotFile{
×
301
                        {
×
302
                                Path:    "issue.md",
×
303
                                Content: formatIssueMarkdown(issue),
×
304
                        },
×
305
                },
×
306
        }
×
307

×
308
        // Fetch and include notes (comments)
×
309
        notes, err := p.client.GetIssueNotes(ctx, ref.IssueIID)
×
310
        if err == nil && len(notes) > 0 {
×
311
                snapshot.Files = append(snapshot.Files, provider.SnapshotFile{
×
312
                        Path:    "notes.md",
×
313
                        Content: formatNotesMarkdown(notes),
×
314
                })
×
315
        }
×
316

317
        return snapshot, nil
×
318
}
319

320
// List lists issues from the project.
321
func (p *Provider) List(ctx context.Context, opts provider.ListOptions) ([]*provider.WorkUnit, error) {
×
322
        // Set up project
×
323
        projectPath := p.config.ProjectPath
×
324
        if projectPath != "" {
×
325
                p.client.SetProjectPath(projectPath)
×
326
        }
×
327

328
        listOpts := &gl.ListProjectIssuesOptions{
×
329
                OrderBy: ptr("created_at"),
×
330
                Sort:    ptr("desc"),
×
331
        }
×
332

×
333
        // Map status
×
334
        if opts.Status != "" {
×
335
                switch opts.Status {
×
336
                case provider.StatusOpen:
×
337
                        listOpts.State = ptr("opened")
×
338
                case provider.StatusClosed:
×
339
                        listOpts.State = ptr("closed")
×
340
                case provider.StatusInProgress, provider.StatusReview, provider.StatusDone:
×
341
                        // GitLab doesn't have these exact states, treat as opened
×
342
                        listOpts.State = ptr("opened")
×
343
                }
344
        }
345

346
        // Map labels
347
        if len(opts.Labels) > 0 {
×
348
                labelOpts := gl.LabelOptions(opts.Labels)
×
349
                listOpts.Labels = &labelOpts
×
350
        }
×
351

352
        // Pagination - Page and PerPage are int64 in ListOptions
353
        if opts.Limit > 0 {
×
354
                listOpts.PerPage = int64(opts.Limit)
×
355
        } else {
×
356
                listOpts.PerPage = 100
×
357
        }
×
358
        if opts.Offset > 0 {
×
359
                page := (int64(opts.Offset) / 100) + 1
×
360
                listOpts.Page = page
×
361
        }
×
362

363
        issues, err := p.client.ListIssues(ctx, listOpts)
×
364
        if err != nil {
×
365
                return nil, err
×
366
        }
×
367

368
        result := make([]*provider.WorkUnit, len(issues))
×
369
        for i, issue := range issues {
×
370
                result[i] = &provider.WorkUnit{
×
371
                        ID:          strconv.FormatInt(issue.IID, 10),
×
372
                        ExternalID:  fmt.Sprintf("%s#%d", p.config.ProjectPath, issue.IID),
×
373
                        Provider:    ProviderName,
×
374
                        Title:       issue.Title,
×
375
                        Description: issue.Description,
×
376
                        Status:      mapGitLabState(issue.State),
×
377
                        Priority:    inferPriorityFromLabels(issue.Labels),
×
378
                        Labels:      issue.Labels,
×
379
                        Assignees:   mapAssignees(issue.Assignees),
×
380
                        CreatedAt:   *issue.CreatedAt,
×
381
                        UpdatedAt:   *issue.UpdatedAt,
×
382
                }
×
383
        }
×
384

385
        return result, nil
×
386
}
387

388
// FetchComments fetches comments for a work unit.
389
func (p *Provider) FetchComments(ctx context.Context, workUnitID string) ([]provider.Comment, error) {
×
390
        ref, err := ParseReference(workUnitID)
×
391
        if err != nil {
×
392
                return nil, err
×
393
        }
×
394

395
        notes, err := p.client.GetIssueNotes(ctx, ref.IssueIID)
×
396
        if err != nil {
×
397
                return nil, err
×
398
        }
×
399

400
        return mapNotes(notes), nil
×
401
}
402

403
// AddComment adds a comment to a work unit.
404
func (p *Provider) AddComment(ctx context.Context, workUnitID string, body string) (*provider.Comment, error) {
×
405
        ref, err := ParseReference(workUnitID)
×
406
        if err != nil {
×
407
                return nil, err
×
408
        }
×
409

410
        note, err := p.client.AddNote(ctx, ref.IssueIID, body)
×
411
        if err != nil {
×
412
                return nil, err
×
413
        }
×
414

415
        author := provider.Person{
×
416
                ID:   strconv.FormatInt(note.Author.ID, 10),
×
417
                Name: note.Author.Username,
×
418
        }
×
419

×
420
        // Email is now a string, not a pointer in the new API
×
421
        if note.Author.Email != "" {
×
422
                author.Email = note.Author.Email
×
423
        }
×
424

425
        var updatedAt time.Time
×
426
        if note.UpdatedAt != nil {
×
427
                updatedAt = *note.UpdatedAt
×
428
        } else if note.CreatedAt != nil {
×
429
                updatedAt = *note.CreatedAt
×
430
        }
×
431

432
        return &provider.Comment{
×
433
                ID:        strconv.FormatInt(note.ID, 10),
×
434
                Body:      note.Body,
×
435
                CreatedAt: *note.CreatedAt,
×
436
                UpdatedAt: updatedAt,
×
437
                Author:    author,
×
438
        }, nil
×
439
}
440

441
// UpdateStatus updates the status of a work unit.
442
func (p *Provider) UpdateStatus(ctx context.Context, workUnitID string, status provider.Status) error {
2✔
443
        ref, err := ParseReference(workUnitID)
2✔
444
        if err != nil {
3✔
445
                return err
1✔
446
        }
1✔
447

448
        var stateEvent *string
1✔
449
        switch status {
1✔
450
        case provider.StatusOpen:
×
451
                stateEvent = ptr("reopen")
×
452
        case provider.StatusClosed:
1✔
453
                stateEvent = ptr("close")
1✔
454
        case provider.StatusInProgress, provider.StatusReview, provider.StatusDone:
×
455
                // GitLab doesn't have these exact states, no state change
×
456
                stateEvent = nil
×
457
        }
458

459
        _, err = p.client.UpdateIssue(ctx, ref.IssueIID, &gl.UpdateIssueOptions{
1✔
460
                StateEvent: stateEvent,
1✔
461
        })
1✔
462

1✔
463
        return err
1✔
464
}
465

466
// AddLabels adds labels to a work unit.
467
func (p *Provider) AddLabels(ctx context.Context, workUnitID string, labels []string) error {
1✔
468
        ref, err := ParseReference(workUnitID)
1✔
469
        if err != nil {
2✔
470
                return err
1✔
471
        }
1✔
472

473
        return p.client.AddLabels(ctx, ref.IssueIID, labels)
×
474
}
475

476
// RemoveLabels removes labels from a work unit.
477
func (p *Provider) RemoveLabels(ctx context.Context, workUnitID string, labels []string) error {
1✔
478
        ref, err := ParseReference(workUnitID)
1✔
479
        if err != nil {
2✔
480
                return err
1✔
481
        }
1✔
482

483
        for _, label := range labels {
×
484
                if err := p.client.RemoveLabel(ctx, ref.IssueIID, label); err != nil {
×
485
                        return err
×
486
                }
×
487
        }
488

489
        return nil
×
490
}
491

492
// CreateWorkUnit creates a new work unit.
493
func (p *Provider) CreateWorkUnit(ctx context.Context, opts provider.CreateWorkUnitOptions) (*provider.WorkUnit, error) {
×
494
        createOpts := &gl.CreateIssueOptions{
×
495
                Title:       ptr(opts.Title),
×
496
                Description: ptr(opts.Description),
×
497
        }
×
498

×
499
        if len(opts.Labels) > 0 {
×
500
                labelOpts := gl.LabelOptions(opts.Labels)
×
501
                createOpts.Labels = &labelOpts
×
502
        }
×
503

504
        // Note: Assignees are not yet implemented - would need user lookup to convert usernames to IDs
505

506
        issue, err := p.client.CreateIssue(ctx, createOpts)
×
507
        if err != nil {
×
508
                return nil, err
×
509
        }
×
510

511
        return p.Fetch(ctx, strconv.FormatInt(issue.IID, 10))
×
512
}
513

514
// GetConfig returns the provider configuration.
515
func (p *Provider) GetConfig() *Config {
1✔
516
        return p.config
1✔
517
}
1✔
518

519
// GetClient returns the GitLab API client.
520
func (p *Provider) GetClient() *Client {
1✔
521
        return p.client
1✔
522
}
1✔
523

524
// --- Helper functions ---
525

526
func mapGitLabState(state string) provider.Status {
4✔
527
        switch state {
4✔
528
        case "opened":
1✔
529
                return provider.StatusOpen
1✔
530
        case "closed":
1✔
531
                return provider.StatusClosed
1✔
532
        default:
2✔
533
                return provider.StatusOpen
2✔
534
        }
535
}
536

537
// labelTypeMap maps GitLab labels to task types.
538
var labelTypeMap = map[string]string{
539
        "bug":           "fix",
540
        "bugfix":        "fix",
541
        "fix":           "fix",
542
        "feature":       "feature",
543
        "enhancement":   "feature",
544
        "docs":          "docs",
545
        "documentation": "docs",
546
        "refactor":      "refactor",
547
        "chore":         "chore",
548
        "test":          "test",
549
        "ci":            "ci",
550
}
551

552
func inferTypeFromLabels(labels []string) string {
14✔
553
        for _, label := range labels {
27✔
554
                name := strings.ToLower(label)
13✔
555
                if t, ok := labelTypeMap[name]; ok {
25✔
556
                        return t
12✔
557
                }
12✔
558
        }
559

560
        return "issue"
2✔
561
}
562

563
// labelPriorityMap maps GitLab labels to priorities.
564
var labelPriorityMap = map[string]provider.Priority{
565
        "critical":      provider.PriorityCritical,
566
        "urgent":        provider.PriorityCritical,
567
        "priority:high": provider.PriorityHigh,
568
        "high-priority": provider.PriorityHigh,
569
        "priority:low":  provider.PriorityLow,
570
        "low-priority":  provider.PriorityLow,
571
}
572

573
func inferPriorityFromLabels(labels []string) provider.Priority {
9✔
574
        for _, label := range labels {
17✔
575
                name := strings.ToLower(label)
8✔
576
                if p, ok := labelPriorityMap[name]; ok {
15✔
577
                        return p
7✔
578
                }
7✔
579
        }
580

581
        return provider.PriorityNormal
2✔
582
}
583

584
func mapAssignees(assignees []*gl.IssueAssignee) []provider.Person {
3✔
585
        persons := make([]provider.Person, len(assignees))
3✔
586
        for i, a := range assignees {
6✔
587
                persons[i] = provider.Person{
3✔
588
                        ID:   strconv.FormatInt(a.ID, 10),
3✔
589
                        Name: a.Username,
3✔
590
                }
3✔
591
        }
3✔
592

593
        return persons
3✔
594
}
595

596
func mapNotes(notes []*gl.Note) []provider.Comment {
4✔
597
        result := make([]provider.Comment, len(notes))
4✔
598
        for i, n := range notes {
8✔
599
                author := provider.Person{
4✔
600
                        ID:   strconv.FormatInt(n.Author.ID, 10),
4✔
601
                        Name: n.Author.Username,
4✔
602
                }
4✔
603

4✔
604
                var updatedAt time.Time
4✔
605
                if n.UpdatedAt != nil {
7✔
606
                        updatedAt = *n.UpdatedAt
3✔
607
                } else if n.CreatedAt != nil {
5✔
608
                        updatedAt = *n.CreatedAt
1✔
609
                }
1✔
610

611
                result[i] = provider.Comment{
4✔
612
                        ID:        strconv.FormatInt(n.ID, 10),
4✔
613
                        Body:      n.Body,
4✔
614
                        CreatedAt: *n.CreatedAt,
4✔
615
                        UpdatedAt: updatedAt,
4✔
616
                        Author:    author,
4✔
617
                }
4✔
618
        }
619

620
        return result
4✔
621
}
622

623
func formatIssueMarkdown(issue *gl.Issue) string {
5✔
624
        var sb strings.Builder
5✔
625

5✔
626
        sb.WriteString(fmt.Sprintf("# #%d: %s\n\n", issue.IID, issue.Title))
5✔
627

5✔
628
        // Metadata
5✔
629
        sb.WriteString("## Metadata\n\n")
5✔
630
        sb.WriteString(fmt.Sprintf("- **State:** %s\n", issue.State))
5✔
631
        if issue.CreatedAt != nil {
10✔
632
                sb.WriteString(fmt.Sprintf("- **Created:** %s\n", issue.CreatedAt.Format(time.RFC3339)))
5✔
633
        }
5✔
634
        if issue.UpdatedAt != nil {
10✔
635
                sb.WriteString(fmt.Sprintf("- **Updated:** %s\n", issue.UpdatedAt.Format(time.RFC3339)))
5✔
636
        }
5✔
637

638
        if issue.Author != nil {
6✔
639
                sb.WriteString(fmt.Sprintf("- **Author:** @%s\n", issue.Author.Username))
1✔
640
        }
1✔
641

642
        if len(issue.Labels) > 0 {
6✔
643
                sb.WriteString(fmt.Sprintf("- **Labels:** %s\n", strings.Join(issue.Labels, ", ")))
1✔
644
        }
1✔
645

646
        if len(issue.Assignees) > 0 {
6✔
647
                assignees := make([]string, len(issue.Assignees))
1✔
648
                for i, a := range issue.Assignees {
3✔
649
                        assignees[i] = "@" + a.Username
2✔
650
                }
2✔
651
                sb.WriteString(fmt.Sprintf("- **Assignees:** %s\n", strings.Join(assignees, ", ")))
1✔
652
        }
653

654
        if issue.WebURL != "" {
6✔
655
                sb.WriteString(fmt.Sprintf("- **URL:** %s\n", issue.WebURL))
1✔
656
        }
1✔
657

658
        // Body
659
        sb.WriteString("\n## Description\n\n")
5✔
660
        if issue.Description != "" {
6✔
661
                sb.WriteString(issue.Description)
1✔
662
        } else {
5✔
663
                sb.WriteString("*No description*")
4✔
664
        }
4✔
665
        sb.WriteString("\n")
5✔
666

5✔
667
        return sb.String()
5✔
668
}
669

670
func formatNotesMarkdown(notes []*gl.Note) string {
3✔
671
        var sb strings.Builder
3✔
672

3✔
673
        sb.WriteString("# Notes\n\n")
3✔
674

3✔
675
        for _, n := range notes {
6✔
676
                authorName := "Unknown"
3✔
677
                if n.Author.Username != "" {
6✔
678
                        authorName = n.Author.Username
3✔
679
                }
3✔
680

681
                sb.WriteString(fmt.Sprintf("## Note by @%s\n\n", authorName))
3✔
682
                if n.CreatedAt != nil {
6✔
683
                        sb.WriteString(fmt.Sprintf("*%s*\n\n", n.CreatedAt.Format(time.RFC3339)))
3✔
684
                }
3✔
685
                sb.WriteString(n.Body)
3✔
686
                sb.WriteString("\n\n---\n\n")
3✔
687
        }
688

689
        return sb.String()
3✔
690
}
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