Skip to content

Commit

Permalink
feat(origin): add gitlab support
Browse files Browse the repository at this point in the history
includes refactor to split each provider into separate files
  • Loading branch information
scottmckendry committed Jan 14, 2025
1 parent fb5357d commit 282dc8a
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 171 deletions.
102 changes: 102 additions & 0 deletions origin/azuredevops.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package origin

import (
"encoding/base64"
"encoding/json"
"fmt"
"html"
"net/http"
"regexp"
"strings"
)

// AzureDevOpsProvider implements IssueProvider for Azure DevOps repositories.
type AzureDevOpsProvider struct {
BaseProvider
org string // Azure DevOps organization
}

// NewAzureDevOpsProvider creates a new Azure DevOps provider with the given configuration.
func NewAzureDevOpsProvider(config Config) *AzureDevOpsProvider {
org := parseAzureDevOpsURL(config.URL)
return &AzureDevOpsProvider{
BaseProvider: NewBaseProvider(config),
org: org,
}
}

// createRequest creates an Azure DevOps API request with appropriate headers.
func (a *AzureDevOpsProvider) createRequest(issueNumber string) (*http.Request, error) {
url := fmt.Sprintf(
"https://dev.azure.com/%s/_apis/wit/workitems/%s?api-version=7.1",
a.org,
issueNumber,
)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
encodedPat := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(":%s", a.config.Token)))
if a.config.Token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Basic %s", encodedPat))
}

return req, nil
}

// GetIssue fetches work item details from Azure DevOps.
func (a *AzureDevOpsProvider) GetIssue(issueNumber string) (*Issue, error) {
req, err := a.createRequest(issueNumber)
if err != nil {
return nil, err
}

resp, err := a.doRequest(req)
if err != nil {
return nil, err
}
if resp == nil {
return nil, nil
}
defer resp.Body.Close()

var azureResponse struct {
ID int `json:"id"`
Fields struct {
Title string `json:"System.Title"`
Description string `json:"System.Description"`
} `json:"fields"`
}

if err := json.NewDecoder(resp.Body).Decode(&azureResponse); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &Issue{
Number: azureResponse.ID,
Title: azureResponse.Fields.Title,
Body: cleanDescription(azureResponse.Fields.Description),
}, nil
}

// parseAzureDevOpsURL extracts organization name from an Azure DevOps URL.
func parseAzureDevOpsURL(url string) (org string) {
url = strings.TrimSuffix(strings.TrimSuffix(url, "/"), ".git")
parts := strings.Split(url, "/")
for i, part := range parts {
if part == "dev.azure.com" && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}

// cleanDescription removes HTML tags and whitespace from the issue description.
func cleanDescription(description string) string {
description = html.UnescapeString(description)
description = regexp.MustCompile(`<[^>]*>`).ReplaceAllString(description, "")
return strings.TrimSpace(description)
}
81 changes: 81 additions & 0 deletions origin/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package origin

import (
"encoding/json"
"fmt"
"net/http"
"strings"
)

// GitHubProvider implements IssueProvider for GitHub repositories.
type GitHubProvider struct {
BaseProvider
owner string // GitHub repository owner
repo string // GitHub repository name
}

// NewGitHubProvider creates a new GitHub provider with the given configuration.
func NewGitHubProvider(config Config) *GitHubProvider {
owner, repo := parseGitHubURL(config.URL)
return &GitHubProvider{
BaseProvider: NewBaseProvider(config),
owner: owner,
repo: repo,
}
}

// createRequest creates a GitHub API request with appropriate headers.
func (g *GitHubProvider) createRequest(issueNumber string) (*http.Request, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues/%s",
g.owner, g.repo, issueNumber)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", "go-changelog")

if g.config.Token != "" {
req.Header.Set("Authorization", "Bearer "+g.config.Token)
}

return req, nil
}

// GetIssue fetches issue details from GitHub.
func (g *GitHubProvider) GetIssue(issueNumber string) (*Issue, error) {
req, err := g.createRequest(issueNumber)
if err != nil {
return nil, err
}

resp, err := g.doRequest(req)
if err != nil {
return nil, err
}
if resp == nil {
return nil, nil
}
defer resp.Body.Close()

var issue Issue
if err := json.NewDecoder(resp.Body).Decode(&issue); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &issue, nil
}

// parseGitHubURL extracts owner and repository name from a GitHub URL.
func parseGitHubURL(url string) (owner, repo string) {
url = strings.TrimSuffix(strings.TrimSuffix(url, "/"), ".git")
parts := strings.Split(url, "/")
for i, part := range parts {
if part == "github.com" && i+2 < len(parts) {
return parts[i+1], parts[i+2]
}
}
return "", ""
}
86 changes: 86 additions & 0 deletions origin/gitlab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package origin

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)

// GitLabProvider implements IssueProvider for GitLab repositories
type GitLabProvider struct {
BaseProvider
project string // URL-encoded project path with namespace (e.g., "group/project")
}

// NewGitLabProvider creates a new GitLab provider with the given configuration
func NewGitLabProvider(config Config) *GitLabProvider {
project := parseGitLabURL(config.URL)
return &GitLabProvider{
BaseProvider: NewBaseProvider(config),
project: project,
}
}

// createRequest creates a GitLab API request with appropriate headers
func (g *GitLabProvider) createRequest(issueNumber string) (*http.Request, error) {
url := fmt.Sprintf("https://gitlab.com/api/v4/projects/%s/issues/%s",
url.PathEscape(g.project), issueNumber)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

if g.config.Token != "" {
req.Header.Set("PRIVATE-TOKEN", g.config.Token)
}

return req, nil
}

// GetIssue fetches issue details from GitLab
func (g *GitLabProvider) GetIssue(issueNumber string) (*Issue, error) {
type GitLabIssue struct {
IID int `json:"iid"`
Title string `json:"title"`
Description string `json:"description"`
}
req, err := g.createRequest(issueNumber)
if err != nil {
return nil, err
}

resp, err := g.doRequest(req)
if err != nil {
return nil, err
}
if resp == nil {
return nil, nil
}
defer resp.Body.Close()

var gitlabIssue GitLabIssue
if err := json.NewDecoder(resp.Body).Decode(&gitlabIssue); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &Issue{
Number: gitlabIssue.IID,
Title: gitlabIssue.Title,
Body: gitlabIssue.Description,
}, nil
}

// parseGitLabURL extracts project path from a GitLab URL
func parseGitLabURL(url string) string {
url = strings.TrimSuffix(strings.TrimSuffix(url, "/"), ".git")
parts := strings.Split(url, "/")
for i, part := range parts {
if part == "gitlab.com" && i+1 < len(parts) {
return strings.Join(parts[i+1:], "/")
}
}
return ""
}
Loading

0 comments on commit 282dc8a

Please sign in to comment.