Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
[Dd]ebug/
[Rr]elease/

# This project
temp.git/

# Compiled resource file
*.res

Expand Down
2 changes: 1 addition & 1 deletion cli/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "golang",
"image": "golang:1.25.1",
"image": "golang:1.25.3",
"customizations": {
"vscode": {
"extensions": ["golang.go"]
Expand Down
345 changes: 345 additions & 0 deletions cli/application/bitbucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
package application

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

type BitbucketApp struct {
config AppConfig
}

func (g *BitbucketApp) GetOrganizations() ([]Organization, error) {
req, _ := http.NewRequest("GET", g.config.ApiUrl+"/2.0/workspaces", nil)
req.SetBasicAuth(g.config.Username, g.config.Password)
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound {
return nil, nil // treat as "no teams"
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}

body, _ := io.ReadAll(resp.Body)

// Bitbucket workspaces response structure
var response struct {
Values []struct {
Slug string `json:"slug"`
DisplayName string `json:"name"`
} `json:"values"`
}

if err := json.Unmarshal(body, &response); err != nil {
return nil, err
}

var orgs []Organization
for _, workspace := range response.Values {
org := Organization{
Name: workspace.Slug,
Description: workspace.DisplayName,
}
orgs = append(orgs, org)
}

return orgs, nil
}

func (g *BitbucketApp) GetAuthenticatedUser() (string, error) {
req, _ := http.NewRequest("GET", g.config.ApiUrl+"/2.0/user", nil)
req.SetBasicAuth(g.config.Username, g.config.Password)
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

// Bitbucket user response structure
var data struct {
Username string `json:"username"`
Nickname string `json:"nickname"`
}
if err := json.Unmarshal(body, &data); err != nil {
return "", err
}

// Bitbucket uses "username" field instead of "login"
if data.Username != "" {
return data.Username, nil
}
return data.Nickname, nil
}

func (g *BitbucketApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) {
var url string
if endpoint == EndpointOrganization {
// For teams/organizations in Bitbucket
url = g.config.ApiUrl + "/2.0/repositories/" + owner
} else {
// For user repositories
if owner == authUser {
url = g.config.ApiUrl + "/2.0/repositories/" + authUser
} else {
url = g.config.ApiUrl + "/2.0/repositories/" + owner
}
}

var repos []Repository

for url != "" {
req, _ := http.NewRequest("GET", url, nil)
req.SetBasicAuth(g.config.Username, g.config.Password)
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)

// Bitbucket repositories response structure
var response struct {
Values []struct {
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
IsPrivate bool `json:"is_private"`
HasWiki bool `json:"has_wiki"`
HasIssues bool `json:"has_issues"`
Owner struct {
Username string `json:"username"`
} `json:"owner"`
Links struct {
Clone []struct {
Name string `json:"name"`
Href string `json:"href"`
} `json:"clone"`
} `json:"links"`
} `json:"values"`
Next string `json:"next"`
}

if err := json.Unmarshal(body, &response); err != nil {
return nil, err
}

for _, item := range response.Values {
repo := Repository{
Name: item.Name,
FullName: item.FullName,
Description: item.Description,
Private: item.IsPrivate,
HasWiki: item.HasWiki,
HasIssues: item.HasIssues,
Owner: User{
Login: item.Owner.Username,
},
}

// Find HTTPS clone URL
for _, link := range item.Links.Clone {
if link.Name == "https" {
repo.CloneUrl = link.Href
break
}
}

repos = append(repos, repo)
}

url = response.Next // Bitbucket uses "next" field for pagination
}

return repos, nil
}

func (g *BitbucketApp) GetIssues(repo Repository) ([]Issue, error) {
url := g.config.ApiUrl + "/2.0/repositories/" + repo.Owner.Login + "/" + repo.Name + "/issues"
var issues []Issue

req, _ := http.NewRequest("GET", url, nil)
req.SetBasicAuth(g.config.Username, g.config.Password)
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)

// Bitbucket issues response structure
var response struct {
Values []struct {
ID int `json:"id"`
Title string `json:"title"`
Content struct {
Raw string `json:"raw"`
} `json:"content"`
State string `json:"state"`
} `json:"values"`
}

if err := json.Unmarshal(body, &response); err != nil {
return nil, err
}

for _, item := range response.Values {
issue := Issue{
Number: item.ID,
Title: item.Title,
Body: item.Content.Raw,
State: item.State,
}
issues = append(issues, issue)
}

return issues, nil
}

func (g *BitbucketApp) CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) {
var url string
if endpoint == EndpointOrganization {
url = g.config.ApiUrl + "/2.0/repositories/" + owner + "/" + source.Name
} else {
url = g.config.ApiUrl + "/2.0/repositories/" + owner + "/" + source.Name
}

payload := map[string]interface{}{
"name": source.Name,
"description": source.Description,
"is_private": source.Private,
"has_wiki": source.HasWiki,
"has_issues": source.HasIssues,
}

jsonData, err := json.Marshal(payload)
if err != nil {
return Repository{}, err
}

req, _ := http.NewRequest("POST", url, strings.NewReader(string(jsonData)))
req.SetBasicAuth(g.config.Username, g.config.Password)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return Repository{}, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
return Repository{}, fmt.Errorf("failed to create repo: %s", resp.Status)
}

body, _ := io.ReadAll(resp.Body)

// Parse response
var response struct {
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
IsPrivate bool `json:"is_private"`
HasWiki bool `json:"has_wiki"`
HasIssues bool `json:"has_issues"`
Owner struct {
Username string `json:"username"`
} `json:"owner"`
Links struct {
Clone []struct {
Name string `json:"name"`
Href string `json:"href"`
} `json:"clone"`
} `json:"links"`
}

if err := json.Unmarshal(body, &response); err != nil {
return Repository{}, err
}

repo := Repository{
Name: response.Name,
FullName: response.FullName,
Description: response.Description,
Private: response.IsPrivate,
HasWiki: response.HasWiki,
HasIssues: response.HasIssues,
Owner: User{
Login: response.Owner.Username,
},
}

// Find HTTPS clone URL
for _, link := range response.Links.Clone {
if link.Name == "https" {
repo.CloneUrl = link.Href
break
}
}

return repo, nil
}

func (g *BitbucketApp) GetApplicationName() string {
return "Bitbucket"
}

func (g *BitbucketApp) GetApiUrl() string {
return g.config.ApiUrl
}

func (g *BitbucketApp) SetApiUrl(url string) {
g.config.ApiUrl = url
}

func (g *BitbucketApp) GetToken() string {
return g.config.Token
}

func (g *BitbucketApp) SetToken(token string) {
g.config.Token = token
}

func (g *BitbucketApp) GetUser() string {
return g.config.User
}

func (g *BitbucketApp) SetUser(user string) {
g.config.User = user
}

func (g *BitbucketApp) GetUsername() string {
return g.config.Username
}

func (g *BitbucketApp) SetUsername(username string) {
g.config.Username = username
}

func (g *BitbucketApp) GetPassword() string {
return g.config.Password
}

func (g *BitbucketApp) SetPassword(password string) {
g.config.Password = password
}

func (g *BitbucketApp) GetEndpoint() ApiEndpoint {
return g.config.Endpoint
}

func (g *BitbucketApp) SetEndpoint(endpoint ApiEndpoint) {
g.config.Endpoint = endpoint
}
Loading
Loading