Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions pkg/github/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,25 @@ func (client *Client) ListAlertsForOrg(ctx context.Context, owner string, opts *
return alerts, resp, err
}

// GetCommitFiles returns the list of files changed in a specific commit.
// Note: the GitHub API returns at most 300 files for a single commit.
func (client *Client) GetCommitFiles(ctx context.Context, owner, repo, sha string, opts *googlegithub.ListOptions) ([]*googlegithub.CommitFile, *googlegithub.Response, error) {
commit, resp, err := client.restClient.Repositories.GetCommit(ctx, owner, repo, sha, opts)
if err != nil {
return nil, nil, addErrorSourceToError(err, resp)
}
return commit.Files, resp, nil
}

// ListPullRequestFiles returns the list of files changed in a specific pull request.
func (client *Client) ListPullRequestFiles(ctx context.Context, owner, repo string, prNumber int, opts *googlegithub.ListOptions) ([]*googlegithub.CommitFile, *googlegithub.Response, error) {
files, resp, err := client.restClient.PullRequests.ListFiles(ctx, owner, repo, prNumber, opts)
if err != nil {
return nil, nil, addErrorSourceToError(err, resp)
}
return files, resp, nil
}

// GetWorkflowUsage returns the workflow usage for a specific workflow.
func (client *Client) GetWorkflowUsage(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange) (models.WorkflowUsage, error) {
actors := make(map[string]struct{}, 0)
Expand Down
8 changes: 8 additions & 0 deletions pkg/github/codescanning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ func (m *mockClient) ListAlertsForOrg(ctx context.Context, owner string, opts *g
return m.mockAlerts, m.mockResponse, nil
}

func (m *mockClient) GetCommitFiles(ctx context.Context, owner, repo, sha string, opts *googlegithub.ListOptions) ([]*googlegithub.CommitFile, *googlegithub.Response, error) {
return nil, nil, nil
}

func (m *mockClient) ListPullRequestFiles(ctx context.Context, owner, repo string, prNumber int, opts *googlegithub.ListOptions) ([]*googlegithub.CommitFile, *googlegithub.Response, error) {
return nil, nil, nil
}

func TestGetCodeScanningAlerts(t *testing.T) {
var (
ctx = context.Background()
Expand Down
144 changes: 144 additions & 0 deletions pkg/github/commit_files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package github

import (
"context"
"fmt"
"time"

googlegithub "github.com/google/go-github/v81/github"
"github.com/grafana/grafana-plugin-sdk-go/data"

"github.com/grafana/github-datasource/pkg/models"
)

// CommitFilesWrapper is a list of commit files returned by the GitHub API
type CommitFilesWrapper []*googlegithub.CommitFile

// Frames converts the list of commit files to a Grafana DataFrame
func (files CommitFilesWrapper) Frames() data.Frames {
frame := data.NewFrame(
"commit_files",
data.NewField("path", nil, []string{}),
data.NewField("additions", nil, []int64{}),
data.NewField("deletions", nil, []int64{}),
data.NewField("changes", nil, []int64{}),
data.NewField("status", nil, []string{}),
data.NewField("previous_filename", nil, []string{}),
)

for _, f := range files {
frame.AppendRow(
f.GetFilename(),
int64(f.GetAdditions()),
int64(f.GetDeletions()),
int64(f.GetChanges()),
f.GetStatus(),
f.GetPreviousFilename(),
)
}

frame.Meta = &data.FrameMeta{PreferredVisualization: data.VisTypeTable}
return data.Frames{frame}
}

// GetCommitFiles fetches the files changed in a specific commit.
// The GitHub REST API returns at most 300 files for a single commit.
func GetCommitFiles(ctx context.Context, client models.Client, opts models.CommitFilesOptions) (CommitFilesWrapper, error) {
if opts.Owner == "" || opts.Repository == "" || opts.Ref == "" {
return nil, nil
}

files, _, err := client.GetCommitFiles(ctx, opts.Owner, opts.Repository, opts.Ref, &googlegithub.ListOptions{
PerPage: 300,
})
if err != nil {
return nil, fmt.Errorf("getting commit files: owner=%s repo=%s sha=%s: %w", opts.Owner, opts.Repository, opts.Ref, err)
}

return CommitFilesWrapper(files), nil
}

// GetPullRequestFiles fetches all files changed in a pull request, handling pagination.
func GetPullRequestFiles(ctx context.Context, client models.Client, opts models.PullRequestFilesOptions) (CommitFilesWrapper, error) {
if opts.Owner == "" || opts.Repository == "" || opts.PRNumber == 0 {
return nil, nil
}

var allFiles []*googlegithub.CommitFile
page := 1

for {
files, resp, err := client.ListPullRequestFiles(ctx, opts.Owner, opts.Repository, int(opts.PRNumber), &googlegithub.ListOptions{
Page: page,
PerPage: 100,
})
if err != nil {
return nil, fmt.Errorf("listing PR files: owner=%s repo=%s pr=%d page=%d: %w", opts.Owner, opts.Repository, opts.PRNumber, page, err)
}

allFiles = append(allFiles, files...)

if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
}

return CommitFilesWrapper(allFiles), nil
}

// CommitWithFiles holds a commit and the files changed in it
type CommitWithFiles struct {
Commit Commit
Files []*googlegithub.CommitFile
}

// CommitsWithFiles is a list of commits each paired with their changed files
type CommitsWithFiles []CommitWithFiles

// Frames converts the list of commits-with-files to a flattened Grafana DataFrame.
// Each row represents one file change within a commit (one row per commit × file).
func (c CommitsWithFiles) Frames() data.Frames {
frame := data.NewFrame(
"commits",
data.NewField("id", nil, []string{}),
data.NewField("author", nil, []string{}),
data.NewField("author_login", nil, []string{}),
data.NewField("author_email", nil, []string{}),
data.NewField("author_company", nil, []string{}),
data.NewField("committed_at", nil, []time.Time{}),
data.NewField("pushed_at", nil, []time.Time{}),
data.NewField("message", nil, []string{}),
data.NewField("file_path", nil, []string{}),
data.NewField("file_additions", nil, []int64{}),
data.NewField("file_deletions", nil, []int64{}),
data.NewField("file_changes", nil, []int64{}),
data.NewField("file_status", nil, []string{}),
data.NewField("previous_filename", nil, []string{}),
)

for _, cwf := range c {
for _, f := range cwf.Files {
frame.AppendRow(
cwf.Commit.OID,
cwf.Commit.Author.Name,
cwf.Commit.Author.User.Login,
cwf.Commit.Author.Email,
cwf.Commit.Author.User.Company,
cwf.Commit.CommittedDate.Time,
cwf.Commit.PushedDate.Time,
string(cwf.Commit.Message),
f.GetFilename(),
int64(f.GetAdditions()),
int64(f.GetDeletions()),
int64(f.GetChanges()),
f.GetStatus(),
f.GetPreviousFilename(),
)
}
}

frame.Meta = &data.FrameMeta{PreferredVisualization: data.VisTypeTable}
return data.Frames{frame}
}

40 changes: 40 additions & 0 deletions pkg/github/commit_files_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package github

import (
"context"

"github.com/grafana/grafana-plugin-sdk-go/backend"

"github.com/grafana/github-datasource/pkg/dfutil"
"github.com/grafana/github-datasource/pkg/models"
)

func (s *QueryHandler) handleCommitFilesQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse {
query := &models.CommitFilesQuery{}
if err := UnmarshalQuery(q.JSON, query); err != nil {
return *err
}
return dfutil.FrameResponseWithError(s.Datasource.HandleCommitFilesQuery(ctx, query, q))
}

// HandleCommitFiles handles the plugin query for files changed in a GitHub commit
func (s *QueryHandler) HandleCommitFiles(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return &backend.QueryDataResponse{
Responses: processQueries(ctx, req, s.handleCommitFilesQuery),
}, nil
}

func (s *QueryHandler) handlePullRequestFilesQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse {
query := &models.PullRequestFilesQuery{}
if err := UnmarshalQuery(q.JSON, query); err != nil {
return *err
}
return dfutil.FrameResponseWithError(s.Datasource.HandlePullRequestFilesQuery(ctx, query, q))
}

// HandlePullRequestFiles handles the plugin query for files changed in a GitHub pull request
func (s *QueryHandler) HandlePullRequestFiles(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return &backend.QueryDataResponse{
Responses: processQueries(ctx, req, s.handlePullRequestFilesQuery),
}, nil
}
Loading
Loading