Skip to content

Commit 12852c5

Browse files
committed
add support for generating github resource links
1 parent dc09550 commit 12852c5

File tree

7 files changed

+278
-2
lines changed

7 files changed

+278
-2
lines changed

scm/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ type (
8888

8989
// Services used for communicating with the API.
9090
Driver Driver
91+
Linker Linker
9192
Contents ContentService
9293
Git GitService
9394
Organizations OrganizationService

scm/driver/github/github.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@ func New(uri string) (*scm.Client, error) {
2525
if !strings.HasSuffix(base.Path, "/") {
2626
base.Path = base.Path + "/"
2727
}
28+
home := base.String()
29+
if home == "https://api.github.com/" {
30+
home = "https://github.com/"
31+
}
2832
client := &wrapper{new(scm.Client)}
2933
client.BaseURL = base
3034
// initialize services
3135
client.Driver = scm.DriverGithub
36+
client.Linker = &linker{home}
3237
client.Contents = &contentService{client}
3338
client.Git = &gitService{client}
3439
client.Issues = &issueService{client}

scm/driver/github/linker.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2017 Drone.IO Inc. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package github
6+
7+
import (
8+
"context"
9+
"fmt"
10+
11+
"github.com/drone/go-scm/scm"
12+
)
13+
14+
type linker struct {
15+
base string
16+
}
17+
18+
// Resource returns a link to the resource.
19+
func (l *linker) Resource(ctx context.Context, repo string, ref scm.Reference) (string, error) {
20+
switch {
21+
case scm.IsTag(ref.Path):
22+
t := scm.TrimRef(ref.Path)
23+
return fmt.Sprintf("%s%s/tree/%s", l.base, repo, t), nil
24+
case scm.IsPullRequest(ref.Path):
25+
d := scm.ExtractPullRequest(ref.Path)
26+
return fmt.Sprintf("%s%s/pull/%d", l.base, repo, d), nil
27+
default:
28+
return fmt.Sprintf("%s%s/commit/%s", l.base, repo, ref.Sha), nil
29+
}
30+
}
31+
32+
// Diff returns a link to the diff.
33+
func (l *linker) Diff(ctx context.Context, repo string, source, target scm.Reference) (string, error) {
34+
if scm.IsPullRequest(target.Path) {
35+
d := scm.ExtractPullRequest(target.Path)
36+
return fmt.Sprintf("%s%s/pull/%d/files", l.base, repo, d), nil
37+
}
38+
39+
s := source.Sha
40+
t := target.Sha
41+
if s == "" {
42+
s = scm.TrimRef(source.Path)
43+
}
44+
if t == "" {
45+
t = scm.TrimRef(target.Path)
46+
}
47+
48+
return fmt.Sprintf("%s%s/compare/%s...%s", l.base, repo, s, t), nil
49+
}

scm/driver/github/linker_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2017 Drone.IO Inc. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package github
6+
7+
import (
8+
"context"
9+
"testing"
10+
11+
"github.com/drone/go-scm/scm"
12+
)
13+
14+
func TestLink(t *testing.T) {
15+
tests := []struct {
16+
path string
17+
sha string
18+
want string
19+
}{
20+
{
21+
path: "refs/heads/master",
22+
sha: "a7389057b0eb027e73b32a81e3c5923a71d01dde",
23+
want: "https://github.com/octocat/hello-world/commit/a7389057b0eb027e73b32a81e3c5923a71d01dde",
24+
},
25+
{
26+
path: "refs/pull/42/head",
27+
sha: "a7389057b0eb027e73b32a81e3c5923a71d01dde",
28+
want: "https://github.com/octocat/hello-world/pull/42",
29+
},
30+
{
31+
path: "refs/tags/v1.0.0",
32+
want: "https://github.com/octocat/hello-world/tree/v1.0.0",
33+
},
34+
}
35+
36+
for _, test := range tests {
37+
client := NewDefault()
38+
ref := scm.Reference{
39+
Path: test.path,
40+
Sha: test.sha,
41+
}
42+
got, err := client.Linker.Resource(context.Background(), "octocat/hello-world", ref)
43+
if err != nil {
44+
t.Error(err)
45+
return
46+
}
47+
want := test.want
48+
if got != want {
49+
t.Errorf("Want link %q, got %q", want, got)
50+
}
51+
}
52+
}
53+
54+
func TestDiff(t *testing.T) {
55+
tests := []struct {
56+
source scm.Reference
57+
target scm.Reference
58+
want string
59+
}{
60+
{
61+
source: scm.Reference{Sha: "a7389057b0eb027e73b32a81e3c5923a71d01dde"},
62+
target: scm.Reference{Sha: "49bbaf4a113bbebfa21cf604cad9aa1503c3f04d"},
63+
want: "https://github.com/octocat/hello-world/compare/a7389057b0eb027e73b32a81e3c5923a71d01dde...49bbaf4a113bbebfa21cf604cad9aa1503c3f04d",
64+
},
65+
{
66+
source: scm.Reference{Path: "refs/heads/master"},
67+
target: scm.Reference{Sha: "49bbaf4a113bbebfa21cf604cad9aa1503c3f04d"},
68+
want: "https://github.com/octocat/hello-world/compare/master...49bbaf4a113bbebfa21cf604cad9aa1503c3f04d",
69+
},
70+
{
71+
source: scm.Reference{Sha: "a7389057b0eb027e73b32a81e3c5923a71d01dde"},
72+
target: scm.Reference{Path: "refs/heads/master"},
73+
want: "https://github.com/octocat/hello-world/compare/a7389057b0eb027e73b32a81e3c5923a71d01dde...master",
74+
},
75+
{
76+
target: scm.Reference{Path: "refs/pull/12/head"},
77+
want: "https://github.com/octocat/hello-world/pull/12/files",
78+
},
79+
}
80+
81+
for _, test := range tests {
82+
client := NewDefault()
83+
got, err := client.Linker.Diff(context.Background(), "octocat/hello-world", test.source, test.target)
84+
if err != nil {
85+
t.Error(err)
86+
return
87+
}
88+
want := test.want
89+
if got != want {
90+
t.Errorf("Want link %q, got %q", want, got)
91+
}
92+
}
93+
}
94+
95+
func TestLink_GitHub_Enterprise(t *testing.T) {
96+
client, _ := New("https://github.acme.com")
97+
ref := scm.Reference{
98+
Path: "refs/heads/master",
99+
Sha: "a7389057b0eb027e73b32a81e3c5923a71d01dde",
100+
}
101+
got, err := client.Linker.Resource(context.Background(), "octocat/hello-world", ref)
102+
if err != nil {
103+
t.Error(err)
104+
return
105+
}
106+
want := "https://github.acme.com/octocat/hello-world/commit/a7389057b0eb027e73b32a81e3c5923a71d01dde"
107+
if got != want {
108+
t.Errorf("Want link %q, got %q", want, got)
109+
}
110+
}

scm/linker.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2017 Drone.IO Inc. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package scm
6+
7+
import "context"
8+
9+
// Linker provides deep links to resources.
10+
type Linker interface {
11+
// Resource returns a link to the resource.
12+
Resource(ctx context.Context, repo string, ref Reference) (string, error)
13+
14+
// Diff returns a link to the diff.
15+
Diff(ctx context.Context, repo string, source, target Reference) (string, error)
16+
}

scm/util.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44

55
package scm
66

7-
import "strings"
7+
import (
8+
"regexp"
9+
"strconv"
10+
"strings"
11+
)
12+
13+
// regular expression to extract the pull request number
14+
// from the git ref (e.g. refs/pulls/{d}/head)
15+
var re = regexp.MustCompile("\\d+")
816

917
// Split splits the full repository name into segments.
1018
func Split(s string) (owner, name string) {
@@ -42,8 +50,24 @@ func ExpandRef(name, prefix string) string {
4250
return prefix + "/" + name
4351
}
4452

53+
// ExtractPullRequest returns name extraced pull request
54+
// number from the reference path.
55+
func ExtractPullRequest(ref string) int {
56+
s := re.FindString(ref)
57+
d, _ := strconv.Atoi(s)
58+
return d
59+
}
60+
4561
// IsTag returns true if the reference path points to
4662
// a tag object.
4763
func IsTag(ref string) bool {
4864
return strings.HasPrefix(ref, "refs/tags/")
4965
}
66+
67+
// IsPullRequest returns true if the reference path points
68+
// to a pull request object.
69+
func IsPullRequest(ref string) bool {
70+
return strings.HasPrefix(ref, "refs/pull/") ||
71+
strings.HasPrefix(ref, "refs/pull-request/") ||
72+
strings.HasPrefix(ref, "refs/merge-requests/")
73+
}

scm/util_test.go

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func TestExpandRef(t *testing.T) {
101101
}
102102
}
103103

104-
func TestIsRef(t *testing.T) {
104+
func TestIsTag(t *testing.T) {
105105
tests := []struct {
106106
name string
107107
tag bool
@@ -122,3 +122,74 @@ func TestIsRef(t *testing.T) {
122122
}
123123
}
124124
}
125+
126+
func TestIsPullRequest(t *testing.T) {
127+
tests := []struct {
128+
name string
129+
tag bool
130+
}{
131+
{
132+
name: "refs/pull/12/head",
133+
tag: true,
134+
},
135+
{
136+
name: "refs/pull/12/merge",
137+
tag: true,
138+
},
139+
{
140+
name: "refs/pull-request/12/head",
141+
tag: true,
142+
},
143+
{
144+
name: "refs/merge-requests/12/head",
145+
tag: true,
146+
},
147+
// not pull requests
148+
{
149+
name: "refs/tags/v1.0.0",
150+
tag: false,
151+
},
152+
{
153+
name: "refs/heads/master",
154+
tag: false,
155+
},
156+
}
157+
for _, test := range tests {
158+
if got, want := IsPullRequest(test.name), test.tag; got != want {
159+
t.Errorf("Got IsPullRequest %v, want %v", got, want)
160+
}
161+
}
162+
}
163+
164+
func TestExtractPullRequest(t *testing.T) {
165+
tests := []struct {
166+
name string
167+
number int
168+
}{
169+
{
170+
name: "refs/pull/12/head",
171+
number: 12,
172+
},
173+
{
174+
name: "refs/pull/12/merge",
175+
number: 12,
176+
},
177+
{
178+
name: "refs/pull-request/12/head",
179+
number: 12,
180+
},
181+
{
182+
name: "refs/merge-requests/12/head",
183+
number: 12,
184+
},
185+
{
186+
name: "refs/heads/master",
187+
number: 0,
188+
},
189+
}
190+
for _, test := range tests {
191+
if got, want := ExtractPullRequest(test.name), test.number; got != want {
192+
t.Errorf("Got pull request number %v, want %v", got, want)
193+
}
194+
}
195+
}

0 commit comments

Comments
 (0)