From 0303dd7c2813e00090a14d2cd4bd098977408e8a Mon Sep 17 00:00:00 2001 From: sue445 Date: Sun, 7 Jul 2019 22:55:49 +0900 Subject: [PATCH 1/4] Add fixture --- gitlab/testdata/project_snippet.json | 17 +++++++++++++++++ gitlab/testdata/project_snippet_note.json | 21 +++++++++++++++++++++ gitlab/testdata/snippet.json | 19 +++++++++++++++++++ gitlab/testdata/snippet_code.rb | 1 + 4 files changed, 58 insertions(+) create mode 100644 gitlab/testdata/project_snippet.json create mode 100644 gitlab/testdata/project_snippet_note.json create mode 100644 gitlab/testdata/snippet.json create mode 100644 gitlab/testdata/snippet_code.rb diff --git a/gitlab/testdata/project_snippet.json b/gitlab/testdata/project_snippet.json new file mode 100644 index 00000000..7510c4a5 --- /dev/null +++ b/gitlab/testdata/project_snippet.json @@ -0,0 +1,17 @@ +{ + "id": 1, + "title": "test", + "file_name": "add.rb", + "description": "Ruby test snippet", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/example/example/snippets/1" +} diff --git a/gitlab/testdata/project_snippet_note.json b/gitlab/testdata/project_snippet_note.json new file mode 100644 index 00000000..e7cf6916 --- /dev/null +++ b/gitlab/testdata/project_snippet_note.json @@ -0,0 +1,21 @@ +{ + "id": 400, + "body": "comment", + "attachment": null, + "author": { + "id": 1, + "username": "pipin", + "email": "admin@example.com", + "name": "Pip", + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/pipin.jpeg", + "created_at": "2013-09-30T13:46:01Z" + }, + "created_at": "2013-10-02T09:22:45Z", + "updated_at": "2013-10-02T10:22:45Z", + "system": true, + "noteable_id": 377, + "noteable_type": "Snippet", + "noteable_iid": 377, + "resolvable": false +} diff --git a/gitlab/testdata/snippet.json b/gitlab/testdata/snippet.json new file mode 100644 index 00000000..ec3ec258 --- /dev/null +++ b/gitlab/testdata/snippet.json @@ -0,0 +1,19 @@ +{ + "id": 1, + "title": "test", + "file_name": "add.rb", + "description": "Ruby test snippet", + "visibility": "private", + "author": { + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + "created_at": "2012-05-23T08:00:58Z" + }, + "expires_at": null, + "updated_at": "2012-06-28T10:52:04Z", + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/snippets/1", +} diff --git a/gitlab/testdata/snippet_code.rb b/gitlab/testdata/snippet_code.rb new file mode 100644 index 00000000..2df4c7fe --- /dev/null +++ b/gitlab/testdata/snippet_code.rb @@ -0,0 +1 @@ +puts 'Hello' From d560b5d44e305a2f0fa9009eddb611e4065ea00c Mon Sep 17 00:00:00 2001 From: sue445 Date: Mon, 8 Jul 2019 00:31:43 +0900 Subject: [PATCH 2/4] Support snippet --- gitlab/project_snippet_fetcher.go | 141 ++++++++++++++++++++++++++++++ gitlab/snippet_fetcher.go | 90 +++++++++++++++++++ gitlab/url_parser.go | 2 + gitlab/url_parser_test.go | 79 +++++++++++++++++ 4 files changed, 312 insertions(+) create mode 100644 gitlab/project_snippet_fetcher.go create mode 100644 gitlab/snippet_fetcher.go diff --git a/gitlab/project_snippet_fetcher.go b/gitlab/project_snippet_fetcher.go new file mode 100644 index 00000000..50d08527 --- /dev/null +++ b/gitlab/project_snippet_fetcher.go @@ -0,0 +1,141 @@ +package gitlab + +import ( + "fmt" + "github.com/xanzy/go-gitlab" + "golang.org/x/sync/errgroup" + "regexp" + "strconv" + "strings" + "time" +) + +type projectSnippetFetcher struct { +} + +func (f *projectSnippetFetcher) fetchPath(path string, client *gitlab.Client, isDebugLogging bool) (*Page, error) { + re := regexp.MustCompile("^([^/]+)/([^/]+)/snippets/(\\d+)") + matched := re.FindStringSubmatch(path) + + if matched == nil { + return nil, nil + } + + projectName := matched[1] + "/" + matched[2] + + var eg errgroup.Group + + var snippet *gitlab.Snippet + authorName := "" + authorAvatarURL := "" + var footerTime *time.Time + var note *gitlab.Note + + snippetID, _ := strconv.Atoi(matched[3]) + + eg.Go(func() error { + var err error + start := time.Now() + snippet, _, err = client.ProjectSnippets.GetSnippet(projectName, snippetID) + + if err != nil { + return err + } + + if isDebugLogging { + duration := time.Now().Sub(start) + fmt.Printf("[DEBUG] projectSnippetFetcher (%s): snippet=%+v\n", duration, snippet) + } + + authorName = snippet.Author.Name + footerTime = snippet.CreatedAt + + re2 := regexp.MustCompile("#note_(\\d+)$") + matched2 := re2.FindStringSubmatch(path) + + if matched2 != nil { + noteID, _ := strconv.Atoi(matched2[1]) + start := time.Now() + note, _, err = client.Notes.GetSnippetNote(projectName, snippetID, noteID) + + if err != nil { + return err + } + + if isDebugLogging { + duration := time.Now().Sub(start) + fmt.Printf("[DEBUG] projectSnippetFetcher (%s): note=%+v\n", duration, note) + } + } + + return nil + }) + + content := "" + + eg.Go(func() error { + var err error + start := time.Now() + rawFile, _, err := client.ProjectSnippets.SnippetContent(projectName, snippetID) + + if err != nil { + return err + } + + content = strings.TrimSpace(string(rawFile)) + + if isDebugLogging { + duration := time.Now().Sub(start) + fmt.Printf("[DEBUG] projectSnippetFetcher (%s): content=%+v\n", duration, content) + } + + return nil + }) + + var project *gitlab.Project + eg.Go(func() error { + var err error + start := time.Now() + project, _, err = client.Projects.GetProject(projectName, nil) + + if err != nil { + return err + } + + if isDebugLogging { + duration := time.Now().Sub(start) + fmt.Printf("[DEBUG] projectSnippetFetcher (%s): project=%+v\n", duration, project) + } + + return nil + }) + + if err := eg.Wait(); err != nil { + return nil, err + } + + description := fmt.Sprintf("```\n%s\n```", content) + canTruncateDescription := false + + if note != nil { + description = note.Body + authorName = note.Author.Name + authorAvatarURL = note.Author.AvatarURL + footerTime = note.CreatedAt + canTruncateDescription = true + } + + page := &Page{ + Title: snippet.FileName, + Description: description, + AuthorName: authorName, + AuthorAvatarURL: authorAvatarURL, + AvatarURL: project.AvatarURL, + CanTruncateDescription: canTruncateDescription, + FooterTitle: project.PathWithNamespace, + FooterURL: project.WebURL, + FooterTime: footerTime, + } + + return page, nil +} diff --git a/gitlab/snippet_fetcher.go b/gitlab/snippet_fetcher.go new file mode 100644 index 00000000..bb82425b --- /dev/null +++ b/gitlab/snippet_fetcher.go @@ -0,0 +1,90 @@ +package gitlab + +import ( + "fmt" + "github.com/xanzy/go-gitlab" + "golang.org/x/sync/errgroup" + "regexp" + "strconv" + "strings" + "time" +) + +type snippetFetcher struct { +} + +func (f *snippetFetcher) fetchPath(path string, client *gitlab.Client, isDebugLogging bool) (*Page, error) { + re := regexp.MustCompile("^snippets/(\\d+)") + matched := re.FindStringSubmatch(path) + + if matched == nil { + return nil, nil + } + + var eg errgroup.Group + + var snippet *gitlab.Snippet + authorName := "" + var footerTime *time.Time + + snippetID, _ := strconv.Atoi(matched[1]) + + eg.Go(func() error { + var err error + start := time.Now() + snippet, _, err = client.Snippets.GetSnippet(snippetID) + + if err != nil { + return err + } + + if isDebugLogging { + duration := time.Now().Sub(start) + fmt.Printf("[DEBUG] snippetFetcher (%s): snippet=%+v\n", duration, snippet) + } + + authorName = snippet.Author.Name + footerTime = snippet.CreatedAt + + return nil + }) + + content := "" + + eg.Go(func() error { + var err error + start := time.Now() + rawFile, _, err := client.Snippets.SnippetContent(snippetID) + + if err != nil { + return err + } + + content = strings.TrimSpace(string(rawFile)) + + if isDebugLogging { + duration := time.Now().Sub(start) + fmt.Printf("[DEBUG] snippetFetcher (%s): content=%+v\n", duration, content) + } + + return nil + }) + + if err := eg.Wait(); err != nil { + return nil, err + } + + page := &Page{ + Title: snippet.FileName, + Description: fmt.Sprintf("```\n%s\n```", content), + AuthorName: authorName, + AuthorAvatarURL: "", + AvatarURL: "", + CanTruncateDescription: false, + FooterTitle: "", + FooterURL: "", + FooterTime: footerTime, + } + + return page, nil +} diff --git a/gitlab/url_parser.go b/gitlab/url_parser.go index 0f85ed41..1085c14a 100644 --- a/gitlab/url_parser.go +++ b/gitlab/url_parser.go @@ -56,11 +56,13 @@ func (p *URLParser) FetchURL(url string) (*Page, error) { path := url[pos:] fetchers := []fetcher{ + &snippetFetcher{}, &issueFetcher{}, &mergeRequestFetcher{}, &jobFetcher{}, &pipelineFetcher{}, &blobFetcher{}, + &projectSnippetFetcher{}, &projectFetcher{}, &userOrGroupFetcher{baseURL: p.baseURL}, } diff --git a/gitlab/url_parser_test.go b/gitlab/url_parser_test.go index 2a450939..2c45326c 100644 --- a/gitlab/url_parser_test.go +++ b/gitlab/url_parser_test.go @@ -78,6 +78,31 @@ func TestGitlabUrlParser_FetchURL(t *testing.T) { "http://example.com/api/v4/projects/diaspora%2Fdiaspora-project-site/pipelines/46", httpmock.NewStringResponder(200, testutil.ReadTestData("testdata/pipeline.json")), ) + httpmock.RegisterResponder( + "GET", + "http://example.com/api/v4/projects/diaspora%2Fdiaspora-project-site/snippets/1", + httpmock.NewStringResponder(200, testutil.ReadTestData("testdata/project_snippet.json")), + ) + httpmock.RegisterResponder( + "GET", + "http://example.com/api/v4/projects/diaspora%2Fdiaspora-project-site/snippets/1/raw", + httpmock.NewStringResponder(200, testutil.ReadTestData("testdata/snippet_code.rb")), + ) + httpmock.RegisterResponder( + "GET", + "http://example.com/api/v4/snippets/3", + httpmock.NewStringResponder(200, testutil.ReadTestData("testdata/project_snippet.json")), + ) + httpmock.RegisterResponder( + "GET", + "http://example.com/api/v4/projects/diaspora%2Fdiaspora-project-site/snippets/1/notes/400", + httpmock.NewStringResponder(200, testutil.ReadTestData("testdata/project_snippet_note.json")), + ) + httpmock.RegisterResponder( + "GET", + "http://example.com/api/v4/snippets/3/raw", + httpmock.NewStringResponder(200, testutil.ReadTestData("testdata/snippet_code.rb")), + ) p, err := NewGitlabURLParser(&URLParserParams{ APIEndpoint: "http://example.com/api/v4", @@ -457,6 +482,60 @@ func TestGitlabUrlParser_FetchURL(t *testing.T) { Color: "#1aaa55", }, }, + { + name: "Project Snippet URL", + args: args{ + url: "http://example.com/diaspora/diaspora-project-site/snippets/1", + }, + want: &Page{ + Title: "add.rb", + Description: "```\nputs 'Hello'\n```", + AuthorName: "John Smith", + AuthorAvatarURL: "", + AvatarURL: "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + CanTruncateDescription: false, + FooterTitle: "diaspora/diaspora-project-site", + FooterURL: "http://example.com/diaspora/diaspora-project-site", + FooterTime: tp(time.Date(2012, 6, 28, 10, 52, 4, 0, time.UTC)), + Color: "", + }, + }, + { + name: "Project Snippet comment URL", + args: args{ + url: "http://example.com/diaspora/diaspora-project-site/snippets/1#note_400", + }, + want: &Page{ + Title: "add.rb", + Description: "comment", + AuthorName: "Pip", + AuthorAvatarURL: "http://localhost:3000/uploads/user/avatar/1/pipin.jpeg", + AvatarURL: "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + CanTruncateDescription: true, + FooterTitle: "diaspora/diaspora-project-site", + FooterURL: "http://example.com/diaspora/diaspora-project-site", + FooterTime: tp(time.Date(2013, 10, 2, 9, 22, 45, 0, time.UTC)), + Color: "", + }, + }, + { + name: "Snippet URL", + args: args{ + url: "http://example.com/snippets/3", + }, + want: &Page{ + Title: "add.rb", + Description: "```\nputs 'Hello'\n```", + AuthorName: "John Smith", + AuthorAvatarURL: "", + AvatarURL: "", + CanTruncateDescription: false, + FooterTitle: "", + FooterURL: "", + FooterTime: tp(time.Date(2012, 6, 28, 10, 52, 4, 0, time.UTC)), + Color: "", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From fe97f38c1f3fa98c4bd659badec4192d503a8675 Mon Sep 17 00:00:00 2001 From: sue445 Date: Mon, 8 Jul 2019 23:00:05 +0900 Subject: [PATCH 3/4] Write README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 02d6f306..02b749c8 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ * e.g. `${GITLAB_BASE_URL}/:namespace/:reponame/pipelines/:id` * Blob URL * e.g. `${GITLAB_BASE_URL}/:namespace/:reponame/blob/:sha1/:filename` +* Project snippet URL + * e.g. `${GITLAB_BASE_URL}/:namespace/:reponame/snippets/:id` +* Snippet URL + * e.g. `${GITLAB_BASE_URL}/snippets/:id` ## Running standalone Download latest binary from https://github.com/sue445/gitpanda/releases From b73f554851afd0bdb5414df179c7235aaf4bf7f2 Mon Sep 17 00:00:00 2001 From: sue445 Date: Mon, 8 Jul 2019 23:30:25 +0900 Subject: [PATCH 4/4] non-project snippet doesn't have both repo title and url --- gitlab/page.go | 20 ++++++++++++- gitlab/page_test.go | 61 ++++++++++++++++++++++++++++++++++++++++ webhook/slack_webhook.go | 2 +- 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 gitlab/page_test.go diff --git a/gitlab/page.go b/gitlab/page.go index ea761e29..06ed5caa 100644 --- a/gitlab/page.go +++ b/gitlab/page.go @@ -1,6 +1,9 @@ package gitlab -import "time" +import ( + "fmt" + "time" +) // Page represents info of GitLab page type Page struct { @@ -15,3 +18,18 @@ type Page struct { FooterTime *time.Time Color string } + +// FormatFooter returns formatted footer for slack +func (p *Page) FormatFooter() string { + if p.FooterURL != "" { + if p.FooterTitle != "" { + return fmt.Sprintf("<%s|%s>", p.FooterURL, p.FooterTitle) + } + return p.FooterURL + } + + if p.FooterTitle != "" { + return p.FooterTitle + } + return "" +} diff --git a/gitlab/page_test.go b/gitlab/page_test.go new file mode 100644 index 00000000..a85cf0c2 --- /dev/null +++ b/gitlab/page_test.go @@ -0,0 +1,61 @@ +package gitlab + +import ( + "testing" +) + +func TestPage_FormatFooter(t *testing.T) { + type fields struct { + FooterTitle string + FooterURL string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "title and url", + fields: fields{ + FooterTitle: "GitHub", + FooterURL: "https://github.com/", + }, + want: "", + }, + { + name: "title only", + fields: fields{ + FooterTitle: "GitHub", + FooterURL: "", + }, + want: "GitHub", + }, + { + name: "url only", + fields: fields{ + FooterTitle: "", + FooterURL: "https://github.com/", + }, + want: "https://github.com/", + }, + { + name: "nothing", + fields: fields{ + FooterTitle: "", + FooterURL: "", + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Page{ + FooterTitle: tt.fields.FooterTitle, + FooterURL: tt.fields.FooterURL, + } + if got := p.FormatFooter(); got != tt.want { + t.Errorf("Page.formatFooter() = %v, want %+v", got, tt.want) + } + }) + } +} diff --git a/webhook/slack_webhook.go b/webhook/slack_webhook.go index c9c5efe9..1155e021 100644 --- a/webhook/slack_webhook.go +++ b/webhook/slack_webhook.go @@ -94,7 +94,7 @@ func (s *SlackWebhook) requestLinkSharedEvent(ev *slackevents.LinkSharedEvent, t AuthorName: page.AuthorName, AuthorIcon: page.AuthorAvatarURL, Text: description, - Footer: fmt.Sprintf("<%s|%s>", page.FooterURL, page.FooterTitle), + Footer: page.FormatFooter(), ThumbURL: page.AvatarURL, }