Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-61]: Show code block previews of Gitlab permalinks #299

Merged
merged 29 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cd9699f
MI-1840: Show code block previews of Github permalinks
shivamjosh May 30, 2022
b9ee4cc
MI-1840: Minor fix
shivamjosh May 30, 2022
aafe1a0
MI-1840: Done review fixes
shivamjosh May 31, 2022
5f49eb4
MI-1840: Added unit test for permalink
shivamjosh Jun 1, 2022
080768e
Merge pull request #4 from Brightscout/MI-1840
shivamjosh Jun 2, 2022
080bcb1
Added the verbose flag to the npm install command in Makefile to fix …
shivamjosh Jun 6, 2022
d02813f
Fixed lint errors
shivamjosh Jun 7, 2022
c50246c
Merge branch 'permalink_preview' of github.com:Brightscout/mattermost…
raghavaggarwal2308 Jul 4, 2022
d83d82d
Merge branch 'master' of github.com:Brightscout/mattermost-plugin-git…
manojmalik20 Jul 25, 2022
c97a8ae
Merge branch 'master' of github.com:mattermost/mattermost-plugin-gitl…
manojmalik20 Jul 25, 2022
84694c9
[MI-1956] Review fixes of Gitlab permalink preview (#13)
Nityanand13 Jul 26, 2022
3191b77
Merge branch 'permalink_preview' of github.com:Brightscout/mattermost…
raghavaggarwal2308 Aug 9, 2022
dee58ed
[MI-2025] Review fixes
raghavaggarwal2308 Aug 9, 2022
61691a5
Modified the comment for function processReplacement
manojmalik20 Aug 9, 2022
acb44ee
Merge pull request #18 from Brightscout/MI-2025
manojmalik20 Aug 9, 2022
8c06a5d
[MI-2080] Review fixes for gitlab permalink_preview
raghavaggarwal2308 Aug 23, 2022
b1e0184
Merge pull request #20 from Brightscout/MI-2080
raghavaggarwal2308 Aug 24, 2022
3bd4716
[MI-2091] Fix failing CI/test
raghavaggarwal2308 Aug 25, 2022
9391e2e
Merge pull request #21 from Brightscout/MI-2091
raghavaggarwal2308 Aug 25, 2022
e227585
Merge branch 'master' of github.com:mattermost/mattermost-plugin-gitl…
raghavaggarwal2308 Mar 6, 2023
2dde8b9
[MI-2993] Review fixes on Gitla PR #299(Show code block preview)
raghavaggarwal2308 Apr 13, 2023
8a0ca55
[MI-2993] Fix testcases
raghavaggarwal2308 Apr 13, 2023
ed82f35
[MI-2993] Review fixes
raghavaggarwal2308 Apr 14, 2023
d8a1d16
[MI-2993] Review fix
raghavaggarwal2308 Apr 17, 2023
4285aab
Merge pull request #35 from Brightscout/MI-2993
raghavaggarwal2308 Apr 17, 2023
fcd62c6
Merge branch 'master' of github.com:mattermost/mattermost-plugin-gitl…
raghavaggarwal2308 Apr 27, 2023
c1cf8f9
[GH-61] Updated description of EnablePrivateRepos config setting.
raghavaggarwal2308 May 7, 2023
2170c8d
Merge branch 'master' of github.com:mattermost/mattermost-plugin-gitl…
raghavaggarwal2308 May 10, 2023
9fc71e6
Merge branch 'master' of github.com:mattermost/mattermost-plugin-gitl…
raghavaggarwal2308 May 24, 2023
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
6 changes: 6 additions & 0 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
"help_text": "(Optional) Allow the plugin to work with private repositories.",
"placeholder": "",
"default": null
},
{
"key": "EnableCodePreview",
"display_name": "Enable Code Previews",
"type": "bool",
"help_text": "(Optional) Allow the plugin to expand permalinks to GitLab files with an actual preview of the linked file."
}
]
}
Expand Down
1 change: 1 addition & 0 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type configuration struct {
EncryptionKey string `json:"encryptionkey"`
GitlabGroup string `json:"gitlabgroup"`
EnablePrivateRepo bool `json:"enableprivaterepo"`
EnableCodePreview bool `json:"enablecodepreview"`
UsePreregisteredApplication bool `json:"usepreregisteredapplication"`
}

Expand Down
20 changes: 10 additions & 10 deletions server/gitlab/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const (

// NewGroupHook creates a webhook associated with a GitLab group
func (g *gitlab) NewGroupHook(ctx context.Context, user *UserInfo, groupName string, webhookOptions *AddWebhookOptions) (*WebhookInfo, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -63,7 +63,7 @@ func (g *gitlab) NewGroupHook(ctx context.Context, user *UserInfo, groupName str

// NewProjectHook creates a webhook associated with a GitLab project
func (g *gitlab) NewProjectHook(ctx context.Context, user *UserInfo, projectID interface{}, webhookOptions *AddWebhookOptions) (*WebhookInfo, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -99,7 +99,7 @@ func (g *gitlab) NewProjectHook(ctx context.Context, user *UserInfo, projectID i

// GetGroupHooks gathers all the group level hooks for a GitLab group.
func (g *gitlab) GetGroupHooks(ctx context.Context, user *UserInfo, owner string) ([]*WebhookInfo, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -206,7 +206,7 @@ func getGroupHookInfo(hook *internGitlab.GroupHook) *WebhookInfo {

// GetProjectHooks gathers all the project level hooks from a single GitLab project.
func (g *gitlab) GetProjectHooks(ctx context.Context, user *UserInfo, owner string, repo string) ([]*WebhookInfo, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand All @@ -227,7 +227,7 @@ func (g *gitlab) GetProjectHooks(ctx context.Context, user *UserInfo, owner stri
}

func (g *gitlab) GetProject(ctx context.Context, user *UserInfo, owner, repo string) (*internGitlab.Project, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand All @@ -247,7 +247,7 @@ func (g *gitlab) GetProject(ctx context.Context, user *UserInfo, owner, repo str
}

func (g *gitlab) GetReviews(ctx context.Context, user *UserInfo) ([]*internGitlab.MergeRequest, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -279,7 +279,7 @@ func (g *gitlab) GetReviews(ctx context.Context, user *UserInfo) ([]*internGitla
}

func (g *gitlab) GetYourPrs(ctx context.Context, user *UserInfo) ([]*internGitlab.MergeRequest, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -318,7 +318,7 @@ func (g *gitlab) GetYourPrs(ctx context.Context, user *UserInfo) ([]*internGitla
}

func (g *gitlab) GetYourAssignments(ctx context.Context, user *UserInfo) ([]*internGitlab.Issue, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -357,7 +357,7 @@ func (g *gitlab) GetYourAssignments(ctx context.Context, user *UserInfo) ([]*int
}

func (g *gitlab) GetUnreads(ctx context.Context, user *UserInfo) ([]*internGitlab.Todo, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -390,7 +390,7 @@ func (g *gitlab) ResolveNamespaceAndProject(
allowPrivate bool,
) (owner string, repo string, err error) {
// Initialize client
client, err := g.gitlabConnect(*userInfo.Token)
client, err := g.GitlabConnect(*userInfo.Token)
if err != nil {
return "", "", err
}
Expand Down
3 changes: 2 additions & 1 deletion server/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var (

// Gitlab is a client to call GitLab api see New() to build one
type Gitlab interface {
GitlabConnect(token oauth2.Token) (*internGitlab.Client, error)
GetCurrentUser(ctx context.Context, userID string, token oauth2.Token) (*UserInfo, error)
GetUserDetails(ctx context.Context, user *UserInfo) (*internGitlab.User, error)
GetProject(ctx context.Context, user *UserInfo, owner, repo string) (*internGitlab.Project, error)
Expand Down Expand Up @@ -73,7 +74,7 @@ func New(gitlabURL string, gitlabGroup string, checkGroup func(projectNameWithGr
return &gitlab{gitlabURL: gitlabURL, gitlabGroup: gitlabGroup, checkGroup: checkGroup}
}

func (g *gitlab) gitlabConnect(token oauth2.Token) (*internGitlab.Client, error) {
func (g *gitlab) GitlabConnect(token oauth2.Token) (*internGitlab.Client, error) {
if g.gitlabURL == "" || strings.EqualFold(g.gitlabURL, Gitlabdotcom) {
return internGitlab.NewOAuthClient(token.AccessToken)
}
Expand Down
15 changes: 15 additions & 0 deletions server/gitlab/mocks/mock_gitlab.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions server/gitlab/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type UserSettings struct {
}

func (g *gitlab) GetCurrentUser(ctx context.Context, userID string, token oauth2.Token) (*UserInfo, error) {
client, err := g.gitlabConnect(token)
client, err := g.GitlabConnect(token)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -55,7 +55,7 @@ func (g *gitlab) GetCurrentUser(ctx context.Context, userID string, token oauth2
}

func (g *gitlab) GetUserDetails(ctx context.Context, user *UserInfo) (*internGitlab.User, error) {
client, err := g.gitlabConnect(*user.Token)
client, err := g.GitlabConnect(*user.Token)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import (
)

func main() {
plugin.ClientMain(&Plugin{})
plugin.ClientMain(NewPlugin())
}
15 changes: 15 additions & 0 deletions server/mocks/mock_gitlab.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

165 changes: 165 additions & 0 deletions server/permalinks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package main

import (
"context"
"encoding/base64"
"fmt"
"strings"
"sync"
"time"

"github.com/xanzy/go-gitlab"
)

// maxPermalinkReplacements sets the maximum limit to the number of
// permalink replacements that can be performed on a single message.
const maxPermalinkReplacements = 10

const permalinkReqTimeout = 5 * time.Second

// maxPreviewLines sets the maximum number of preview lines that will be shown
// while replacing a permalink.
const maxPreviewLines = 10

// permalinkLineContext shows the number of lines before and after to show
// if the link points to a single line.
const permalinkLineContext = 3

// replacement holds necessary info to replace GitLab permalinks
// in messages with a code preview block.
type replacement struct {
index int // Index of the permalink in the string
word string // The permalink
permalinkData permalinkInfo
}

type permalinkInfo struct { // Holds the necessary metadata of a permalink
haswww string
commit string
user string
repo string
path string
line string
}

// getPermalinkReplacements returns the permalink replacements that need to be performed
// on a message. The returned slice is sorted by the index in ascending order.
func (p *Plugin) getPermalinkReplacements(msg string) []replacement {
// Find the permalinks from the msg using a regex
matches := gitlabPermalinkRegex.FindAllStringSubmatch(msg, -1)
indices := gitlabPermalinkRegex.FindAllStringIndex(msg, -1)
var replacements []replacement
for i, m := range matches {
// Have a limit on the number of replacements to do
if i > maxPermalinkReplacements {
break
}
word := m[0]
index := indices[i][0]
r := replacement{
index: index,
word: word,
}
// Ignore if the word is inside a link
if isInsideLink(msg, index) {
continue
}
// Populate the permalinkInfo with the extracted groups of the regex
for j, name := range gitlabPermalinkRegex.SubexpNames() {
if j == 0 {
continue
}
switch name {
case "haswww":
r.permalinkData.haswww = m[j]
case "user":
r.permalinkData.user = m[j]
case "repo":
r.permalinkData.repo = m[j]
case "commit":
r.permalinkData.commit = m[j]
case "path":
r.permalinkData.path = m[j]
case "line":
r.permalinkData.line = m[j]
}
}
replacements = append(replacements, r)
}
return replacements
}

// processReplacement processes a single replacement and stores the resulting markdown at the given index of the given array. Multiple goroutines are executing this function but all the goroutines concurrently modify unique indices of the given array, and concurrently modifying unique slice elements is not racy.
func (p *Plugin) processReplacement(r replacement, glClient *gitlab.Client, wg *sync.WaitGroup, markdownForPermalink []string, index int) {
levb marked this conversation as resolved.
Show resolved Hide resolved
defer wg.Done()

// Get the file contents
opts := gitlab.GetFileOptions{
Ref: &r.permalinkData.commit,
}
projectPath := fmt.Sprintf("%s/%s", r.permalinkData.user, r.permalinkData.repo)
_, cancel := context.WithTimeout(context.Background(), permalinkReqTimeout)
file, _, err := glClient.RepositoryFiles.GetFile(projectPath, r.permalinkData.path, &opts)
defer cancel()
if err != nil {
p.API.LogDebug("Error while fetching file contents", "error", err.Error(), "path", r.permalinkData.path)
return
}
// If this is not a file, ignore.
if file == nil {
p.API.LogWarn("Permalink is not a file", "file", r.permalinkData.path)
return
}

decoded, err := base64.StdEncoding.DecodeString(file.Content)
if err != nil {
p.API.LogDebug("Error while decoding file contents", "error", err.Error(), "path", r.permalinkData.path)
return
}
// Get the required lines.
start, end := getLineNumbers(r.permalinkData.line)
// Bad anchor tag, ignore.
if start == -1 || end == -1 {
return
}

isTruncated := false
if end-start > maxPreviewLines {
end = start + maxPreviewLines
isTruncated = true
}

lines, err := filterLines(string(decoded), start, end)
if err != nil {
p.API.LogDebug("Error while filtering lines", "error", err.Error(), "path", r.permalinkData.path)
}

if lines == "" {
p.API.LogDebug("Line numbers out of range. Skipping.", "file", r.permalinkData.path, "start", start, "end", end)
return
}

markdownForPermalink[index] = getCodeMarkdown(r.permalinkData.user, r.permalinkData.repo, r.permalinkData.path, r.word, lines, isTruncated)
}

// makeReplacements performs the given replacements on the msg and returns
// the new msg. The replacements slice needs to be sorted by the index in ascending order.
// Ref: mattermost plugin github (/~https://github.com/mattermost/mattermost-plugin-github/blob/fcc50523c17c2670cb595a8038864a8a7f7fd1e5/server/plugin/permalinks.go#L90)
func (p *Plugin) makeReplacements(msg string, replacements []replacement, glClient *gitlab.Client) string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this code adapted from the GitHub plugin's implementation of this feature?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put a comment linking to the original function using a GitHub permalink?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

// Iterating the slice in reverse to preserve the replacement indices.
wg := new(sync.WaitGroup)
markdownForPermalink := make([]string, len(replacements))
for i := len(replacements) - 1; i >= 0; i-- {
wg.Add(1)
go p.processReplacement(replacements[i], glClient, wg, markdownForPermalink, i)
}
wg.Wait()
for i := len(replacements) - 1; i >= 0; i-- {
r := replacements[i]
if markdownForPermalink[i] != "" {
// Replace word in msg starting from r.index only once.
msg = msg[:r.index] + strings.Replace(msg[r.index:], r.word, markdownForPermalink[i], 1)
}
}
return msg
}
Loading