Merge pull request #64 from defsprite/resolve-commit-sha
Resolve tag to commit SHA
This commit is contained in:
@@ -92,6 +92,7 @@ Also creates the following files:
|
|||||||
* `tag` containing the git tag name of the release being fetched.
|
* `tag` containing the git tag name of the release being fetched.
|
||||||
* `version` containing the version determined by the git tag of the release being fetched.
|
* `version` containing the version determined by the git tag of the release being fetched.
|
||||||
* `body` containing the body text of the release.
|
* `body` containing the body text of the release.
|
||||||
|
* `commit_sha` containing the commit SHA the tag is pointing to.
|
||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
|
@@ -109,6 +109,15 @@ type FakeGitHub struct {
|
|||||||
result1 *url.URL
|
result1 *url.URL
|
||||||
result2 error
|
result2 error
|
||||||
}
|
}
|
||||||
|
GetRefStub func(tag string) (*github.Reference, error)
|
||||||
|
getRefMutex sync.RWMutex
|
||||||
|
getRefArgsForCall []struct {
|
||||||
|
tag string
|
||||||
|
}
|
||||||
|
getRefReturns struct {
|
||||||
|
result1 *github.Reference
|
||||||
|
result2 error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fake *FakeGitHub) ListReleases() ([]*github.RepositoryRelease, error) {
|
func (fake *FakeGitHub) ListReleases() ([]*github.RepositoryRelease, error) {
|
||||||
@@ -466,4 +475,37 @@ func (fake *FakeGitHub) GetZipballLinkReturns(result1 *url.URL, result2 error) {
|
|||||||
}{result1, result2}
|
}{result1, result2}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fake *FakeGitHub) GetRef(tag string) (*github.Reference, error) {
|
||||||
|
fake.getRefMutex.Lock()
|
||||||
|
fake.getRefArgsForCall = append(fake.getRefArgsForCall, struct {
|
||||||
|
tag string
|
||||||
|
}{tag})
|
||||||
|
fake.getRefMutex.Unlock()
|
||||||
|
if fake.GetRefStub != nil {
|
||||||
|
return fake.GetRefStub(tag)
|
||||||
|
} else {
|
||||||
|
return fake.getRefReturns.result1, fake.getRefReturns.result2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeGitHub) GetRefCallCount() int {
|
||||||
|
fake.getRefMutex.RLock()
|
||||||
|
defer fake.getRefMutex.RUnlock()
|
||||||
|
return len(fake.getRefArgsForCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeGitHub) GetRefArgsForCall(i int) string {
|
||||||
|
fake.getRefMutex.RLock()
|
||||||
|
defer fake.getRefMutex.RUnlock()
|
||||||
|
return fake.getRefArgsForCall[i].tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeGitHub) GetRefReturns(result1 *github.Reference, result2 error) {
|
||||||
|
fake.GetRefStub = nil
|
||||||
|
fake.getRefReturns = struct {
|
||||||
|
result1 *github.Reference
|
||||||
|
result2 error
|
||||||
|
}{result1, result2}
|
||||||
|
}
|
||||||
|
|
||||||
var _ resource.GitHub = new(FakeGitHub)
|
var _ resource.GitHub = new(FakeGitHub)
|
||||||
|
10
github.go
10
github.go
@@ -31,6 +31,7 @@ type GitHub interface {
|
|||||||
|
|
||||||
GetTarballLink(tag string) (*url.URL, error)
|
GetTarballLink(tag string) (*url.URL, error)
|
||||||
GetZipballLink(tag string) (*url.URL, error)
|
GetZipballLink(tag string) (*url.URL, error)
|
||||||
|
GetRef(tag string) (*github.Reference, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GitHubClient struct {
|
type GitHubClient struct {
|
||||||
@@ -247,6 +248,15 @@ func (g *GitHubClient) GetZipballLink(tag string) (*url.URL, error) {
|
|||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GitHubClient) GetRef(tag string) (*github.Reference, error) {
|
||||||
|
ref, res, err := g.client.Git.GetRef(context.TODO(), g.owner, g.repository, "tags/"+tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
|
||||||
func oauthClient(ctx context.Context, source Source) (*http.Client, error) {
|
func oauthClient(ctx context.Context, source Source) (*http.Client, error) {
|
||||||
ts := oauth2.StaticTokenSource(&oauth2.Token{
|
ts := oauth2.StaticTokenSource(&oauth2.Token{
|
||||||
AccessToken: source.AccessToken,
|
AccessToken: source.AccessToken,
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
"github.com/onsi/gomega/ghttp"
|
"github.com/onsi/gomega/ghttp"
|
||||||
|
"github.com/google/go-github/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("GitHub Client", func() {
|
var _ = Describe("GitHub Client", func() {
|
||||||
@@ -161,6 +162,7 @@ var _ = Describe("GitHub Client", func() {
|
|||||||
Repository: "concourse",
|
Repository: "concourse",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
Context("When GitHub's rate limit has been exceeded", func() {
|
Context("When GitHub's rate limit has been exceeded", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
rateLimitResponse := `{
|
rateLimitResponse := `{
|
||||||
@@ -188,5 +190,86 @@ var _ = Describe("GitHub Client", func() {
|
|||||||
Expect(err.Error()).To(ContainSubstring("API rate limit exceeded for 127.0.0.1. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)"))
|
Expect(err.Error()).To(ContainSubstring("API rate limit exceeded for 127.0.0.1. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Context("When GitHub responds successfully", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
server.AppendHandlers(
|
||||||
|
ghttp.CombineHandlers(
|
||||||
|
ghttp.VerifyRequest("GET", "/repos/concourse/concourse/releases/tags/some-tag"),
|
||||||
|
ghttp.RespondWith(200, `{ "id": 1 }`),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("Returns a populated github.RepositoryRelease", func() {
|
||||||
|
expectedRelease := &github.RepositoryRelease{
|
||||||
|
ID: github.Int(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
release, err := client.GetReleaseByTag("some-tag")
|
||||||
|
|
||||||
|
Ω(err).ShouldNot(HaveOccurred())
|
||||||
|
Expect(release).To(Equal(expectedRelease))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("GetRef", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
source = Source{
|
||||||
|
Owner: "concourse",
|
||||||
|
Repository: "concourse",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When GitHub's rate limit has been exceeded", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
rateLimitResponse := `{
|
||||||
|
"message": "API rate limit exceeded for 127.0.0.1. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
|
||||||
|
"documentation_url": "https://developer.github.com/v3/#rate-limiting"
|
||||||
|
}`
|
||||||
|
|
||||||
|
rateLimitHeaders := http.Header(map[string][]string{
|
||||||
|
"X-RateLimit-Limit": {"60"},
|
||||||
|
"X-RateLimit-Remaining": {"0"},
|
||||||
|
"X-RateLimit-Reset": {"1377013266"},
|
||||||
|
})
|
||||||
|
|
||||||
|
server.AppendHandlers(
|
||||||
|
ghttp.CombineHandlers(
|
||||||
|
ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/refs/tags/some-tag"),
|
||||||
|
ghttp.RespondWith(403, rateLimitResponse, rateLimitHeaders),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("Returns an appropriate error", func() {
|
||||||
|
_, err := client.GetRef("some-tag")
|
||||||
|
Expect(err).ToNot(BeNil())
|
||||||
|
Expect(err.Error()).To(ContainSubstring("API rate limit exceeded for 127.0.0.1. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("When GitHub responds successfully", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
server.AppendHandlers(
|
||||||
|
ghttp.CombineHandlers(
|
||||||
|
ghttp.VerifyRequest("GET", "/repos/concourse/concourse/git/refs/tags/some-tag"),
|
||||||
|
ghttp.RespondWith(200, `{ "ref": "refs/tags/some-tag" }`),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("Returns a populated github.Reference", func() {
|
||||||
|
expectedReference := &github.Reference{
|
||||||
|
Ref: github.String("refs/tags/some-tag"),
|
||||||
|
}
|
||||||
|
|
||||||
|
reference, err := client.GetRef("some-tag")
|
||||||
|
|
||||||
|
Ω(err).ShouldNot(HaveOccurred())
|
||||||
|
Expect(reference).To(Equal(expectedReference))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -32,6 +32,7 @@ func (c *InCommand) Run(destDir string, request InRequest) (InResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var foundRelease *github.RepositoryRelease
|
var foundRelease *github.RepositoryRelease
|
||||||
|
var commitSHA string
|
||||||
|
|
||||||
if request.Version.Tag != "" {
|
if request.Version.Tag != "" {
|
||||||
foundRelease, err = c.github.GetReleaseByTag(request.Version.Tag)
|
foundRelease, err = c.github.GetReleaseByTag(request.Version.Tag)
|
||||||
@@ -61,6 +62,19 @@ func (c *InCommand) Run(destDir string, request InRequest) (InResponse, error) {
|
|||||||
return InResponse{}, err
|
return InResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commitPath := filepath.Join(destDir, "commit_sha")
|
||||||
|
commitSHA, err = c.resolveTagToCommitSHA(*foundRelease.TagName)
|
||||||
|
if err != nil {
|
||||||
|
return InResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if commitSHA != "" {
|
||||||
|
err = ioutil.WriteFile(commitPath, []byte(commitSHA), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return InResponse{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if foundRelease.Body != nil && *foundRelease.Body != "" {
|
if foundRelease.Body != nil && *foundRelease.Body != "" {
|
||||||
body := *foundRelease.Body
|
body := *foundRelease.Body
|
||||||
bodyPath := filepath.Join(destDir, "body")
|
bodyPath := filepath.Join(destDir, "body")
|
||||||
@@ -133,7 +147,7 @@ func (c *InCommand) Run(destDir string, request InRequest) (InResponse, error) {
|
|||||||
|
|
||||||
return InResponse{
|
return InResponse{
|
||||||
Version: versionFromRelease(foundRelease),
|
Version: versionFromRelease(foundRelease),
|
||||||
Metadata: metadataFromRelease(foundRelease),
|
Metadata: metadataFromRelease(foundRelease, commitSHA),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,3 +196,17 @@ func (c *InCommand) downloadFile(url, destPath string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *InCommand) resolveTagToCommitSHA(tag string) (string, error) {
|
||||||
|
reference, err := c.github.GetRef(tag)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if *reference.Object.Type != "commit" {
|
||||||
|
fmt.Fprintln(c.writer, "could not resolve tag '%s' to commit: returned type is not 'commit' - only lightweight tags are supported")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return *reference.Object.SHA, err
|
||||||
|
}
|
||||||
|
@@ -86,10 +86,23 @@ var _ = Describe("In Command", func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildTagRef := func(tagRef, commitSHA string) *github.Reference {
|
||||||
|
return &github.Reference{
|
||||||
|
Ref: github.String(tagRef),
|
||||||
|
URL: github.String("https://example.com"),
|
||||||
|
Object: &github.GitObject{
|
||||||
|
Type: github.String("commit"),
|
||||||
|
SHA: github.String(commitSHA),
|
||||||
|
URL: github.String("https://example.com"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Context("when there is a tagged release", func() {
|
Context("when there is a tagged release", func() {
|
||||||
Context("when a present version is specified", func() {
|
Context("when a present version is specified", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
githubClient.GetReleaseByTagReturns(buildRelease(1, "v0.35.0", false), nil)
|
githubClient.GetReleaseByTagReturns(buildRelease(1, "v0.35.0", false), nil)
|
||||||
|
githubClient.GetRefReturns(buildTagRef("v0.35.0", "f28085a4a8f744da83411f5e09fd7b1709149eee"), nil)
|
||||||
|
|
||||||
githubClient.ListReleaseAssetsReturns([]*github.ReleaseAsset{
|
githubClient.ListReleaseAssetsReturns([]*github.ReleaseAsset{
|
||||||
buildAsset(0, "example.txt"),
|
buildAsset(0, "example.txt"),
|
||||||
@@ -129,6 +142,7 @@ var _ = Describe("In Command", func() {
|
|||||||
resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"},
|
resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"},
|
||||||
resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true},
|
resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true},
|
||||||
resource.MetadataPair{Name: "tag", Value: "v0.35.0"},
|
resource.MetadataPair{Name: "tag", Value: "v0.35.0"},
|
||||||
|
resource.MetadataPair{Name: "commit_sha", Value: "f28085a4a8f744da83411f5e09fd7b1709149eee"},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -157,6 +171,10 @@ var _ = Describe("In Command", func() {
|
|||||||
Ω(err).ShouldNot(HaveOccurred())
|
Ω(err).ShouldNot(HaveOccurred())
|
||||||
Ω(string(contents)).Should(Equal("0.35.0"))
|
Ω(string(contents)).Should(Equal("0.35.0"))
|
||||||
|
|
||||||
|
contents, err = ioutil.ReadFile(path.Join(destDir, "commit_sha"))
|
||||||
|
Ω(err).ShouldNot(HaveOccurred())
|
||||||
|
Ω(string(contents)).Should(Equal("f28085a4a8f744da83411f5e09fd7b1709149eee"))
|
||||||
|
|
||||||
contents, err = ioutil.ReadFile(path.Join(destDir, "body"))
|
contents, err = ioutil.ReadFile(path.Join(destDir, "body"))
|
||||||
Ω(err).ShouldNot(HaveOccurred())
|
Ω(err).ShouldNot(HaveOccurred())
|
||||||
Ω(string(contents)).Should(Equal("*markdown*"))
|
Ω(string(contents)).Should(Equal("*markdown*"))
|
||||||
@@ -343,6 +361,7 @@ var _ = Describe("In Command", func() {
|
|||||||
resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"},
|
resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"},
|
||||||
resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true},
|
resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true},
|
||||||
resource.MetadataPair{Name: "tag", Value: "v0.35.0"},
|
resource.MetadataPair{Name: "tag", Value: "v0.35.0"},
|
||||||
|
resource.MetadataPair{Name: "commit_sha", Value: "f28085a4a8f744da83411f5e09fd7b1709149eee"},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -416,6 +435,7 @@ var _ = Describe("In Command", func() {
|
|||||||
Context("which has a tag", func() {
|
Context("which has a tag", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
githubClient.GetReleaseReturns(buildRelease(1, "v0.35.0", true), nil)
|
githubClient.GetReleaseReturns(buildRelease(1, "v0.35.0", true), nil)
|
||||||
|
githubClient.GetRefReturns(buildTagRef("v0.35.0", "f28085a4a8f744da83411f5e09fd7b1709149eee"), nil)
|
||||||
|
|
||||||
inRequest.Version = &resource.Version{ID: "1"}
|
inRequest.Version = &resource.Version{ID: "1"}
|
||||||
inResponse, inErr = command.Run(destDir, inRequest)
|
inResponse, inErr = command.Run(destDir, inRequest)
|
||||||
@@ -435,6 +455,7 @@ var _ = Describe("In Command", func() {
|
|||||||
resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"},
|
resource.MetadataPair{Name: "name", Value: "release-name", URL: "http://google.com"},
|
||||||
resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true},
|
resource.MetadataPair{Name: "body", Value: "*markdown*", Markdown: true},
|
||||||
resource.MetadataPair{Name: "tag", Value: "v0.35.0"},
|
resource.MetadataPair{Name: "tag", Value: "v0.35.0"},
|
||||||
|
resource.MetadataPair{Name: "commit_sha", Value: "f28085a4a8f744da83411f5e09fd7b1709149eee"},
|
||||||
resource.MetadataPair{Name: "draft", Value: "true"},
|
resource.MetadataPair{Name: "draft", Value: "true"},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
@@ -447,6 +468,10 @@ var _ = Describe("In Command", func() {
|
|||||||
contents, err = ioutil.ReadFile(path.Join(destDir, "version"))
|
contents, err = ioutil.ReadFile(path.Join(destDir, "version"))
|
||||||
Ω(err).ShouldNot(HaveOccurred())
|
Ω(err).ShouldNot(HaveOccurred())
|
||||||
Ω(string(contents)).Should(Equal("0.35.0"))
|
Ω(string(contents)).Should(Equal("0.35.0"))
|
||||||
|
|
||||||
|
contents, err = ioutil.ReadFile(path.Join(destDir, "commit_sha"))
|
||||||
|
Ω(err).ShouldNot(HaveOccurred())
|
||||||
|
Ω(string(contents)).Should(Equal("f28085a4a8f744da83411f5e09fd7b1709149eee"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -479,6 +504,7 @@ var _ = Describe("In Command", func() {
|
|||||||
It("does not create the tag and version files", func() {
|
It("does not create the tag and version files", func() {
|
||||||
Ω(path.Join(destDir, "tag")).ShouldNot(BeAnExistingFile())
|
Ω(path.Join(destDir, "tag")).ShouldNot(BeAnExistingFile())
|
||||||
Ω(path.Join(destDir, "version")).ShouldNot(BeAnExistingFile())
|
Ω(path.Join(destDir, "version")).ShouldNot(BeAnExistingFile())
|
||||||
|
Ω(path.Join(destDir, "commit_sha")).ShouldNot(BeAnExistingFile())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -510,6 +536,7 @@ var _ = Describe("In Command", func() {
|
|||||||
It("does not create the tag and version files", func() {
|
It("does not create the tag and version files", func() {
|
||||||
Ω(path.Join(destDir, "tag")).ShouldNot(BeAnExistingFile())
|
Ω(path.Join(destDir, "tag")).ShouldNot(BeAnExistingFile())
|
||||||
Ω(path.Join(destDir, "version")).ShouldNot(BeAnExistingFile())
|
Ω(path.Join(destDir, "version")).ShouldNot(BeAnExistingFile())
|
||||||
|
Ω(path.Join(destDir, "commit_sha")).ShouldNot(BeAnExistingFile())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -2,7 +2,7 @@ package resource
|
|||||||
|
|
||||||
import "github.com/google/go-github/github"
|
import "github.com/google/go-github/github"
|
||||||
|
|
||||||
func metadataFromRelease(release *github.RepositoryRelease) []MetadataPair {
|
func metadataFromRelease(release *github.RepositoryRelease, commitSHA string) []MetadataPair {
|
||||||
metadata := []MetadataPair{}
|
metadata := []MetadataPair{}
|
||||||
|
|
||||||
if release.Name != nil {
|
if release.Name != nil {
|
||||||
@@ -40,6 +40,13 @@ func metadataFromRelease(release *github.RepositoryRelease) []MetadataPair {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if commitSHA != "" {
|
||||||
|
metadata = append(metadata, MetadataPair{
|
||||||
|
Name: "commit_sha",
|
||||||
|
Value: commitSHA,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if *release.Draft {
|
if *release.Draft {
|
||||||
metadata = append(metadata, MetadataPair{
|
metadata = append(metadata, MetadataPair{
|
||||||
Name: "draft",
|
Name: "draft",
|
||||||
|
@@ -145,7 +145,7 @@ func (c *OutCommand) Run(sourceDir string, request OutRequest) (OutResponse, err
|
|||||||
|
|
||||||
return OutResponse{
|
return OutResponse{
|
||||||
Version: versionFromRelease(release),
|
Version: versionFromRelease(release),
|
||||||
Metadata: metadataFromRelease(release),
|
Metadata: metadataFromRelease(release, ""),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user