diff --git a/git.go b/git.go index d50cd18..a60e33d 100644 --- a/git.go +++ b/git.go @@ -11,6 +11,7 @@ import ( "github.com/go-git/go-git/v5" "github.com/kevinburke/ssh_config" "github.com/muesli/gitty/vcs" + "github.com/muesli/gitty/vcs/bitbucketcloud" "github.com/muesli/gitty/vcs/gitea" "github.com/muesli/gitty/vcs/github" "github.com/muesli/gitty/vcs/gitlab" @@ -82,6 +83,9 @@ func guessClient(host string) (Client, error) { if strings.Contains(host, "invent.kde.org") { return gitlab.NewClient(host, token, true) } + if strings.Contains(host, "bitbucket.org") { + return bitbucketcloud.NewClient(token) + } var client Client var err error diff --git a/go.mod b/go.mod index 8886e6b..800cd53 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/dustin/go-humanize v1.0.0 github.com/go-git/go-git/v5 v5.4.2 github.com/kevinburke/ssh_config v1.1.0 + github.com/ktrysmt/go-bitbucket v0.9.34 + github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/muesli/gamut v0.2.1-0.20210907032934-daa621f1be71 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.9.0 diff --git a/go.sum b/go.sum index a993fd6..db66447 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,7 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -153,6 +154,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o= github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= @@ -165,12 +168,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ktrysmt/go-bitbucket v0.9.34 h1:l1Ten1SEoNV9rFrfdfRAWfVbXgbVTTqApl16lIdMRJo= +github.com/ktrysmt/go-bitbucket v0.9.34/go.mod h1:FWxy2UK7GlK5b0NSJGc5hPqnssVlkNnsChvyuOf/Xno= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -179,6 +186,10 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY= github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 h1:p4A2Jx7Lm3NV98VRMKlyWd3nqf8obft8NfXlAUmqd3I= github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY= @@ -215,6 +226,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= @@ -274,6 +286,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -304,6 +317,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -321,6 +335,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -430,6 +445,7 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/vcs/bitbucketcloud/bitbucketcloud.go b/vcs/bitbucketcloud/bitbucketcloud.go new file mode 100644 index 0000000..51b3e8a --- /dev/null +++ b/vcs/bitbucketcloud/bitbucketcloud.go @@ -0,0 +1,231 @@ +package bitbucketcloud + +import ( + "fmt" + "strings" + "time" + + "github.com/ktrysmt/go-bitbucket" + "github.com/mitchellh/mapstructure" + "github.com/muesli/gitty/vcs" +) + +type Client struct { + api *bitbucket.Client +} + +func NewClient(token string) (*Client, error) { + split := strings.SplitN(token, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("failed to get username and app password for bitbucket. Make sure to provide both username and password separated by a colon") + } + client := bitbucket.NewBasicAuth(split[0], split[1]) + + return &Client{ + api: client, + }, nil +} + +func (c *Client) GetUsername() (string, error) { + user, err := c.api.User.Profile() + if err != nil { + return "", err + } + return user.Username, nil +} + +func (c *Client) Issues(owner string, name string) ([]vcs.Issue, error) { + type IssueResponse struct { + Values []struct { + ID float64 + Title string + CreatedOn string `mapstructure:"created_on"` + Content struct { + Raw string + } + } + } + var i []vcs.Issue + issueResponse, err := c.api.Repositories.Issues.Gets(&bitbucket.IssuesOptions{Owner: owner, RepoSlug: name}) + if err != nil { + // TODO: Find out if issue tracker is disabled, return zero issues instead of the error + if strings.Contains(err.Error(), "404") { + return i, nil + } + return i, err + } + + var issueResponseTyped IssueResponse + err = mapstructure.Decode(issueResponse, &issueResponseTyped) + if err != nil { + return i, err + } + + for _, issue := range issueResponseTyped.Values { + createdAt, err := time.Parse("2006-01-02T15:04:05.999999Z07:00", issue.CreatedOn) + if err != nil { + return i, err + } + i = append(i, vcs.Issue{ + ID: int(issue.ID), + Title: issue.Title, + Body: issue.Content.Raw, + CreatedAt: createdAt, + }) + } + // TODO: some issues are considered closed, like duplicates + return i, nil +} + +func (c *Client) PullRequests(owner string, name string) ([]vcs.PullRequest, error) { + type PullRequestResponse struct { + Values []struct { + ID float64 + Title string + CreatedOn string `mapstructure:"created_on"` + Content struct { + Raw string + } + } + } + var prs []vcs.PullRequest + prResponse, err := c.api.Repositories.PullRequests.Gets(&bitbucket.PullRequestsOptions{Owner: owner, RepoSlug: name}) + if err != nil { + return prs, err + } + + var prResponseTyped PullRequestResponse + err = mapstructure.Decode(prResponse, &prResponseTyped) + if err != nil { + return prs, err + } + + for _, pr := range prResponseTyped.Values { + createdAt, err := time.Parse("2006-01-02T15:04:05.999999Z07:00", pr.CreatedOn) + if err != nil { + return prs, err + } + prs = append(prs, vcs.PullRequest{ + ID: int(pr.ID), + Title: pr.Title, + Body: pr.Content.Raw, + CreatedAt: createdAt, + }) + } + return prs, nil +} + +func (c *Client) Repository(owner string, name string) (vcs.Repo, error) { + repo, err := c.api.Repositories.Repository.Get(&bitbucket.RepositoryOptions{ + Owner: owner, + RepoSlug: name, + }) + if err != nil { + return vcs.Repo{}, err + } + html, _ := repo.Links["html"].(map[string]interface{})["href"].(string) + + return vcs.Repo{ + Owner: repo.Owner["display_name"].(string), + Name: repo.Name, + NameWithOwner: "foo", + URL: html, + Description: repo.Description, + }, nil +} + +func (c *Client) Repositories(owner string) ([]vcs.Repo, error) { + return nil, nil +} + +func (c *Client) Branches(owner string, name string) ([]vcs.Branch, error) { + type BranchResponse struct { + Branches []struct { + Name string + Target struct { + Hash string + Author struct { + User struct { + DisplayName string `mapstructure:"display_name"` + } + } + Date string + Message string + } + } + } + var branches []vcs.Branch + branchesResponse, err := c.api.Repositories.Repository.ListBranches(&bitbucket.RepositoryBranchOptions{Owner: owner, RepoSlug: name}) + if err != nil { + return branches, err + } + + var branchesResponseTyped BranchResponse + err = mapstructure.Decode(branchesResponse, &branchesResponseTyped) + if err != nil { + return branches, err + } + + for _, branch := range branchesResponseTyped.Branches { + date, err := time.Parse("2006-01-02T15:04:05.999999Z07:00", branch.Target.Date) + if err != nil { + return branches, err + } + branches = append(branches, vcs.Branch{ + Name: branch.Name, + LastCommit: vcs.Commit{ + ID: branch.Target.Hash, + CommittedAt: date, + MessageHeadline: strings.SplitN(branch.Target.Message, "\n", 2)[0], + Author: branch.Target.Author.User.DisplayName, + }, + }) + } + return branches, nil +} + +func (c *Client) History(repo vcs.Repo, max int, since time.Time) ([]vcs.Commit, error) { + var com []vcs.Commit + type CommitResponse struct { + Values []struct { + Date string + Message string + Hash string + + Author struct { + User struct { + DisplayName string `mapstructure:"display_name"` + } + } + } + } + + commitResponse, err := c.api.Repositories.Commits.GetCommits(&bitbucket.CommitsOptions{ + Owner: repo.Owner, + RepoSlug: repo.Name, + }) + var commitResponseTyped CommitResponse + err = mapstructure.Decode(commitResponse, &commitResponseTyped) + if err != nil { + return com, err + } + + for _, commit := range commitResponseTyped.Values { + date, err := time.Parse("2006-01-02T15:04:05.999999Z07:00", commit.Date) + if err != nil { + return com, err + } + com = append(com, vcs.Commit{ + ID: commit.Hash, + MessageHeadline: strings.SplitN(commit.Message, "\n", 2)[0], + CommittedAt: date, + Author: commit.Author.User.DisplayName, + }) + } + + return com, nil +} + +func (c *Client) IssueURL(owner string, name string, number int) string { + return fmt.Sprintf("https://bitbucket.org/%s/%s/%d", owner, name, number) +}