-
Notifications
You must be signed in to change notification settings - Fork 38
Add user contribution stats table #533
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
Open
salekseev
wants to merge
3
commits into
turbot:main
Choose a base branch
from
salekseev:add-user-contribution-stats
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| --- | ||
| title: "Steampipe Table: github_user_contribution_stats - Query GitHub user contributions summary using SQL" | ||
| description: "Query GitHub user contribution summaries and calendar data from the GraphQL ContributionsCollection." | ||
| folder: "User" | ||
| --- | ||
|
|
||
| # Table: github_user_contribution_stats - Query GitHub user contributions summary using SQL | ||
|
|
||
| The `github_user_contribution_stats` table provides access to GitHub's ContributionsCollection data for a user, including total contribution counts and the contribution calendar (weeks/days). This makes it possible to build dashboards and reports similar to a user's public contribution graph. | ||
|
|
||
| ## Table Usage Guide | ||
|
|
||
| The table is scoped to a single user per query. Optionally specify `from_date` and `to_date` to constrain the contribution window. | ||
|
|
||
| **Important Notes** | ||
| - You must specify the `login` column in the `where` clause. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Get contribution summary for a user | ||
|
|
||
| ```sql+postgres | ||
| select | ||
| total_commit_contributions, | ||
| total_issue_contributions, | ||
| total_pull_request_contributions, | ||
| total_pull_request_review_contributions, | ||
| total_repositories_with_contributed_commits | ||
| from | ||
| github_user_contribution_stats | ||
| where | ||
| login = 'octocat'; | ||
| ``` | ||
|
|
||
| ```sql+sqlite | ||
| select | ||
| total_commit_contributions, | ||
| total_issue_contributions, | ||
| total_pull_request_contributions, | ||
| total_pull_request_review_contributions, | ||
| total_repositories_with_contributed_commits | ||
| from | ||
| github_user_contribution_stats | ||
| where | ||
| login = 'octocat'; | ||
| ``` | ||
|
|
||
| ### Get contribution calendar for a date range | ||
|
|
||
| ```sql+postgres | ||
| select | ||
| contribution_calendar | ||
| from | ||
| github_user_contribution_stats | ||
| where | ||
| login = 'octocat' | ||
| and from_date = '2025-01-01' | ||
| and to_date = '2025-12-31'; | ||
| ``` | ||
|
|
||
| ```sql+sqlite | ||
| select | ||
| contribution_calendar | ||
| from | ||
| github_user_contribution_stats | ||
| where | ||
| login = 'octocat' | ||
| and from_date = '2025-01-01' | ||
| and to_date = '2025-12-31'; | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package github | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "slices" | ||
|
|
||
| "github.com/shurcooL/githubv4" | ||
| "github.com/turbot/steampipe-plugin-github/github/models" | ||
| "github.com/turbot/steampipe-plugin-sdk/v5/plugin" | ||
| ) | ||
|
|
||
| func appendContributionColumnIncludes(m *map[string]interface{}, cols []string) { | ||
| (*m)["includeContributionCalendar"] = githubv4.Boolean(slices.Contains(cols, "contribution_calendar")) | ||
| (*m)["includeCommitContributionsByRepository"] = githubv4.Boolean(slices.Contains(cols, "commit_contributions_by_repository")) | ||
| } | ||
|
|
||
| func extractContributionsCollectionFromHydrateItem(h *plugin.HydrateData) (models.ContributionsCollection, error) { | ||
| if collection, ok := h.Item.(models.ContributionsCollection); ok { | ||
| return collection, nil | ||
| } | ||
| return models.ContributionsCollection{}, fmt.Errorf("unable to parse hydrate item %v as ContributionsCollection", h.Item) | ||
| } | ||
|
|
||
| func contributionHydrateCalendar(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { | ||
| collection, err := extractContributionsCollectionFromHydrateItem(h) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return collection.ContributionCalendar, nil | ||
| } | ||
|
|
||
| func contributionHydrateCommitContributionsByRepository(_ context.Context, _ *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { | ||
| collection, err := extractContributionsCollectionFromHydrateItem(h) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return collection.CommitContributionsByRepository, nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package models | ||
|
|
||
| import "github.com/shurcooL/githubv4" | ||
|
|
||
| type ContributionCalendar struct { | ||
| TotalContributions int `graphql:"totalContributions" json:"total_contributions"` | ||
| Weeks []ContributionWeek `graphql:"weeks" json:"weeks"` | ||
| } | ||
|
|
||
| type ContributionWeek struct { | ||
| ContributionDays []ContributionDay `graphql:"contributionDays" json:"contribution_days"` | ||
| FirstDay githubv4.Date `graphql:"firstDay" json:"first_day"` | ||
| } | ||
|
|
||
| type ContributionDay struct { | ||
| Color string `graphql:"color" json:"color"` | ||
| ContributionCount int `graphql:"contributionCount" json:"contribution_count"` | ||
| ContributionLevel githubv4.ContributionLevel `graphql:"contributionLevel" json:"contribution_level"` | ||
| Date githubv4.Date `graphql:"date" json:"date"` | ||
| Weekday int `graphql:"weekday" json:"weekday"` | ||
| } | ||
|
|
||
| type CommitContributionsByRepository struct { | ||
| Repository struct { | ||
| NameWithOwner string `graphql:"nameWithOwner" json:"name_with_owner"` | ||
| Url string `graphql:"url" json:"url"` | ||
| } `graphql:"repository" json:"repository"` | ||
| Contributions struct { | ||
| TotalCount int `graphql:"totalCount" json:"total_count"` | ||
| } `graphql:"contributions" json:"contributions"` | ||
| } | ||
|
|
||
| type ContributionsCollection struct { | ||
| TotalCommitContributions int `graphql:"totalCommitContributions" json:"total_commit_contributions"` | ||
| TotalIssueContributions int `graphql:"totalIssueContributions" json:"total_issue_contributions"` | ||
| TotalPullRequestContributions int `graphql:"totalPullRequestContributions" json:"total_pull_request_contributions"` | ||
| TotalPullRequestReviewContributions int `graphql:"totalPullRequestReviewContributions" json:"total_pull_request_review_contributions"` | ||
| TotalRepositoriesWithContributedCommits int `graphql:"totalRepositoriesWithContributedCommits" json:"total_repositories_with_contributed_commits"` | ||
| ContributionCalendar ContributionCalendar `graphql:"contributionCalendar @include(if:$includeContributionCalendar)" json:"contribution_calendar,omitempty"` | ||
| CommitContributionsByRepository []CommitContributionsByRepository `graphql:"commitContributionsByRepository(maxRepositories: $maxRepositories) @include(if:$includeCommitContributionsByRepository)" json:"commit_contributions_by_repository,omitempty"` | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package github | ||
|
|
||
| import ( | ||
| "context" | ||
| "strings" | ||
|
|
||
| "github.com/shurcooL/githubv4" | ||
| "github.com/turbot/steampipe-plugin-github/github/models" | ||
| "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" | ||
| "github.com/turbot/steampipe-plugin-sdk/v5/plugin" | ||
| "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" | ||
| ) | ||
|
|
||
| func tableGitHubUserContributionStats() *plugin.Table { | ||
| return &plugin.Table{ | ||
| Name: "github_user_contribution_stats", | ||
| Description: "Contribution summary and calendar data for a GitHub user.", | ||
| List: &plugin.ListConfig{ | ||
| KeyColumns: []*plugin.KeyColumn{ | ||
| {Name: "login", Require: plugin.Required}, | ||
| {Name: "from_date", Require: plugin.Optional, Operators: []string{"="}}, | ||
| {Name: "to_date", Require: plugin.Optional, Operators: []string{"="}}, | ||
| }, | ||
| ShouldIgnoreError: isNotFoundError([]string{"404"}), | ||
| Hydrate: tableGitHubUserContributionStatsList, | ||
| }, | ||
| Columns: commonColumns([]*plugin.Column{ | ||
| {Name: "login", Type: proto.ColumnType_STRING, Description: "The login name of the user.", Transform: transform.FromQual("login")}, | ||
| {Name: "from_date", Type: proto.ColumnType_TIMESTAMP, Description: "Start date for the contribution window.", Transform: transform.FromQual("from_date")}, | ||
| {Name: "to_date", Type: proto.ColumnType_TIMESTAMP, Description: "End date for the contribution window.", Transform: transform.FromQual("to_date")}, | ||
| {Name: "total_commit_contributions", Type: proto.ColumnType_INT, Description: "Total count of commit contributions.", Transform: transform.FromField("TotalCommitContributions")}, | ||
| {Name: "total_issue_contributions", Type: proto.ColumnType_INT, Description: "Total count of issue contributions.", Transform: transform.FromField("TotalIssueContributions")}, | ||
| {Name: "total_pull_request_contributions", Type: proto.ColumnType_INT, Description: "Total count of pull request contributions.", Transform: transform.FromField("TotalPullRequestContributions")}, | ||
| {Name: "total_pull_request_review_contributions", Type: proto.ColumnType_INT, Description: "Total count of pull request review contributions.", Transform: transform.FromField("TotalPullRequestReviewContributions")}, | ||
| {Name: "total_repositories_with_contributed_commits", Type: proto.ColumnType_INT, Description: "Total count of repositories with contributed commits.", Transform: transform.FromField("TotalRepositoriesWithContributedCommits")}, | ||
| {Name: "contribution_calendar", Type: proto.ColumnType_JSON, Description: "Contribution calendar with weeks and days.", Hydrate: contributionHydrateCalendar, Transform: transform.FromValue().NullIfZero()}, | ||
| {Name: "commit_contributions_by_repository", Type: proto.ColumnType_JSON, Description: "Commit contributions aggregated by repository.", Hydrate: contributionHydrateCommitContributionsByRepository, Transform: transform.FromValue().NullIfZero()}, | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| func tableGitHubUserContributionStatsList(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { | ||
| login := d.EqualsQuals["login"].GetStringValue() | ||
|
|
||
| var fromDate *githubv4.DateTime | ||
| var toDate *githubv4.DateTime | ||
|
|
||
| if d.EqualsQuals["from_date"] != nil { | ||
| fromTime := d.EqualsQuals["from_date"].GetTimestampValue().AsTime() | ||
| fromDate = githubv4.NewDateTime(githubv4.DateTime{Time: fromTime}) | ||
| } | ||
|
|
||
| if d.EqualsQuals["to_date"] != nil { | ||
| toTime := d.EqualsQuals["to_date"].GetTimestampValue().AsTime() | ||
| toDate = githubv4.NewDateTime(githubv4.DateTime{Time: toTime}) | ||
| } | ||
|
|
||
| var query struct { | ||
| RateLimit models.RateLimit | ||
| User struct { | ||
| ContributionsCollection models.ContributionsCollection `graphql:"contributionsCollection(from: $from, to: $to)"` | ||
| } `graphql:"user(login: $login)"` | ||
| } | ||
|
|
||
| variables := map[string]interface{}{ | ||
| "login": githubv4.String(login), | ||
| "from": (*githubv4.DateTime)(nil), | ||
| "to": (*githubv4.DateTime)(nil), | ||
| "maxRepositories": githubv4.Int(100), | ||
| } | ||
|
salekseev marked this conversation as resolved.
|
||
|
|
||
| if fromDate != nil { | ||
| variables["from"] = fromDate | ||
| } | ||
| if toDate != nil { | ||
| variables["to"] = toDate | ||
| } | ||
|
|
||
| appendContributionColumnIncludes(&variables, d.QueryContext.Columns) | ||
|
|
||
| client := connectV4(ctx, d) | ||
| err := client.Query(ctx, &query, variables) | ||
| plugin.Logger(ctx).Debug(rateLimitLogString("github_user_contribution_stats", &query.RateLimit)) | ||
| if err != nil { | ||
| plugin.Logger(ctx).Error("github_user_contribution_stats", "api_error", err) | ||
| if strings.Contains(err.Error(), "Could not resolve to a User with the login of") { | ||
| return nil, nil | ||
| } | ||
| return nil, err | ||
| } | ||
|
|
||
| d.StreamListItem(ctx, query.User.ContributionsCollection) | ||
|
|
||
| return nil, nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.