Skip to content

Time range and path filtering for list commits, this should address #197 #783

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
wants to merge 2 commits into
base: main
Choose a base branch
from
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
44 changes: 43 additions & 1 deletion pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"strings"
"time"

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/raw"
Expand Down Expand Up @@ -96,7 +97,7 @@ func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (too
// ListCommits creates a tool to get commits of a branch in a repository.
func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_commits",
mcp.WithDescription(t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).")),
mcp.WithDescription(t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository. Can be filtered by author, date range (since/until), or file path. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_LIST_COMMITS_USER_TITLE", "List commits"),
ReadOnlyHint: ToBoolPtr(true),
Expand All @@ -115,6 +116,15 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t
mcp.WithString("author",
mcp.Description("Author username or email address to filter commits by"),
),
mcp.WithString("since",
mcp.Description("Only return commits after this date (ISO 8601 format, e.g., '2024-01-01T00:00:00Z')"),
),
mcp.WithString("until",
mcp.Description("Only return commits before this date (ISO 8601 format, e.g., '2024-12-31T23:59:59Z')"),
),
mcp.WithString("path",
mcp.Description("Only return commits that touch the specified file path"),
),
WithPagination(),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Expand All @@ -134,6 +144,18 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
since, err := OptionalParam[string](request, "since")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
until, err := OptionalParam[string](request, "until")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
path, err := OptionalParam[string](request, "path")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
pagination, err := OptionalPaginationParams(request)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
Expand All @@ -143,15 +165,35 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t
if perPage == 0 {
perPage = 30
}

opts := &github.CommitsListOptions{
SHA: sha,
Author: author,
Path: path,
ListOptions: github.ListOptions{
Page: pagination.Page,
PerPage: perPage,
},
}

// Parse since time if provided
if since != "" {
sinceTime, err := time.Parse(time.RFC3339, since)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("invalid since date format (use ISO 8601/RFC3339): %s", err.Error())), nil
}
opts.Since = sinceTime
}

// Parse until time if provided
if until != "" {
untilTime, err := time.Parse(time.RFC3339, until)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("invalid until date format (use ISO 8601/RFC3339): %s", err.Error())), nil
}
opts.Until = untilTime
}

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
Expand Down
155 changes: 155 additions & 0 deletions pkg/github/repositories_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,9 @@ func Test_ListCommits(t *testing.T) {
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "sha")
assert.Contains(t, tool.InputSchema.Properties, "author")
assert.Contains(t, tool.InputSchema.Properties, "since")
assert.Contains(t, tool.InputSchema.Properties, "until")
assert.Contains(t, tool.InputSchema.Properties, "path")
assert.Contains(t, tool.InputSchema.Properties, "page")
assert.Contains(t, tool.InputSchema.Properties, "perPage")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
Expand Down Expand Up @@ -826,6 +829,150 @@ func Test_ListCommits(t *testing.T) {
expectError: false,
expectedCommits: mockCommits,
},
{
name: "successful commits fetch with time filtering",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposCommitsByOwnerByRepo,
expectQueryParams(t, map[string]string{
"since": "2024-01-01T00:00:00Z",
"until": "2024-12-31T23:59:59Z",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockCommits),
),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"since": "2024-01-01T00:00:00Z",
"until": "2024-12-31T23:59:59Z",
},
expectError: false,
expectedCommits: mockCommits,
},
{
name: "successful commits fetch with only since parameter",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposCommitsByOwnerByRepo,
expectQueryParams(t, map[string]string{
"since": "2024-06-01T00:00:00Z",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockCommits),
),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"since": "2024-06-01T00:00:00Z",
},
expectError: false,
expectedCommits: mockCommits,
},
{
name: "successful commits fetch with only until parameter",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposCommitsByOwnerByRepo,
expectQueryParams(t, map[string]string{
"until": "2024-06-30T23:59:59Z",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockCommits),
),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"until": "2024-06-30T23:59:59Z",
},
expectError: false,
expectedCommits: mockCommits,
},
{
name: "invalid since date format",
mockedClient: mock.NewMockedHTTPClient(
// No HTTP requests expected due to early validation error
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"since": "invalid-date-format",
},
expectError: false, // This returns a tool error, not a Go error
expectedErrMsg: "invalid since date format",
},
{
name: "invalid until date format",
mockedClient: mock.NewMockedHTTPClient(
// No HTTP requests expected due to early validation error
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"until": "2024/12/31", // Wrong format
},
expectError: false, // This returns a tool error, not a Go error
expectedErrMsg: "invalid until date format",
},
{
name: "successful commits fetch with path filtering",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposCommitsByOwnerByRepo,
expectQueryParams(t, map[string]string{
"path": "src/main.go",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockCommits),
),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"path": "src/main.go",
},
expectError: false,
expectedCommits: mockCommits,
},
{
name: "successful commits fetch with path and time filtering",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposCommitsByOwnerByRepo,
expectQueryParams(t, map[string]string{
"path": "docs/README.md",
"since": "2024-01-01T00:00:00Z",
"until": "2024-12-31T23:59:59Z",
"author": "testuser",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockCommits),
),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"path": "docs/README.md",
"since": "2024-01-01T00:00:00Z",
"until": "2024-12-31T23:59:59Z",
"author": "testuser",
},
expectError: false,
expectedCommits: mockCommits,
},
{
name: "commits fetch fails",
mockedClient: mock.NewMockedHTTPClient(
Expand Down Expand Up @@ -867,6 +1014,14 @@ func Test_ListCommits(t *testing.T) {
return
}

if tc.expectedErrMsg != "" {
require.NoError(t, err)
require.True(t, result.IsError)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
return
}

require.NoError(t, err)
require.False(t, result.IsError)

Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy