Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

get_file_content Match Paths in Git Tree if Full Path Unknown#650

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
LuluBeatson merged 11 commits intomainfromlulu/get-file
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
11 commits
Select commitHold shift + click to select a range
3269af4
add contingency to match path in git tree
LuluBeatsonJul 7, 2025
be2f36f
resolveGitReference helper
LuluBeatsonJul 7, 2025
37a6088
fix: handling of directories
LuluBeatsonJul 8, 2025
588866f
Test_filterPaths
LuluBeatsonJul 8, 2025
1cc6f7c
filterPaths - trailing slashes
LuluBeatsonJul 8, 2025
11d8a45
Merge remote-tracking branch 'origin/main' into lulu/get-file
LuluBeatsonJul 8, 2025
12c2cfe
fix: close response body, improve error messages, docs
LuluBeatsonJul 8, 2025
8c40155
update tool result message about resolved git ref
LuluBeatsonJul 8, 2025
bf673f8
unit test cases for filterPaths maxResults param
LuluBeatsonJul 8, 2025
43d72ed
Merge remote-tracking branch 'origin/main' into lulu/get-file
LuluBeatsonJul 11, 2025
3aea320
resolveGitReference - NewGitHubAPIErrorToCtx
LuluBeatsonJul 11, 2025
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
158 changes: 114 additions & 44 deletionspkg/github/repositories.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,6 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"

ghErrors "github.com/github/github-mcp-server/pkg/errors"
Expand DownExpand Up@@ -495,33 +494,18 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
return mcp.NewToolResultError(err.Error()), nil
}

rawOpts := &raw.ContentOpts{}

if strings.HasPrefix(ref, "refs/pull/") {
prNumber := strings.TrimSuffix(strings.TrimPrefix(ref, "refs/pull/"), "/head")
if len(prNumber) > 0 {
// fetch the PR from the API to get the latest commit and use SHA
githubClient, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
prNum, err := strconv.Atoi(prNumber)
if err != nil {
return nil, fmt.Errorf("invalid pull request number: %w", err)
}
pr, _, err := githubClient.PullRequests.Get(ctx, owner, repo, prNum)
if err != nil {
return nil, fmt.Errorf("failed to get pull request: %w", err)
}
sha = pr.GetHead().GetSHA()
ref = ""
}
client, err := getClient(ctx)
if err != nil {
return mcp.NewToolResultError("failed to get GitHub client"), nil
}

rawOpts.SHA = sha
rawOpts.Ref = ref
rawOpts, err := resolveGitReference(ctx, client, owner, repo, ref, sha)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s", err)), nil
}

// If the path is (most likely) not to be a directory, we will first try to get the raw content from the GitHub raw content API.
// If the path is (most likely) not to be a directory, we will
// first try to get the raw content from the GitHub raw content API.
if path != "" && !strings.HasSuffix(path, "/") {

rawClient, err := getRawClient(ctx)
Expand DownExpand Up@@ -580,36 +564,51 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
}
}

client, err := getClient(ctx)
if err != nil {
return mcp.NewToolResultError("failed to get GitHub client"), nil
}

if sha != "" {
ref = sha
if rawOpts.SHA != "" {
ref = rawOpts.SHA
}
if strings.HasSuffix(path, "/") {
opts := &github.RepositoryContentGetOptions{Ref: ref}
_, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
if err != nil {
return mcp.NewToolResultError("failed to get file contents"), nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != 200 {
body, err := io.ReadAll(resp.Body)
if err == nil && resp.StatusCode == http.StatusOK {
defer func() { _ = resp.Body.Close() }()
r, err := json.Marshal(dirContent)
if err != nil {
return mcp.NewToolResultError("failed toread response body"), nil
return mcp.NewToolResultError("failed tomarshal response"), nil
}
return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s",string(body))), nil
return mcp.NewToolResultText(string(r)), nil
}
}

// The path does not point to a file or directory.
// Instead let's try to find it in the Git Tree by matching the end of the path.

// Step 1: Get Git Tree recursively
tree, resp, err := client.Git.GetTree(ctx, owner, repo, ref, true)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get git tree",
resp,
err,
), nil
}
defer func() { _ = resp.Body.Close() }()

r, err := json.Marshal(dirContent)
// Step 2: Filter tree for matching paths
const maxMatchingFiles = 3
matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
if len(matchingFiles) > 0 {
matchingFilesJSON, err := json.Marshal(matchingFiles)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil
}
resolvedRefs, err := json.Marshal(rawOpts)
if err != nil {
return mcp.NewToolResultError("failed to marshalresponse"), nil
return mcp.NewToolResultError(fmt.Sprintf("failed to marshalresolved refs: %s", err)), nil
}
return mcp.NewToolResultText(string(r)), nil
return mcp.NewToolResultText(fmt.Sprintf("Path did not point to a file or directory, but resolved git ref to %s with possible path matches: %s", resolvedRefs, matchingFilesJSON)), nil
}

return mcp.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil
}
}
Expand DownExpand Up@@ -1293,3 +1292,74 @@ func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool m
return mcp.NewToolResultText(string(r)), nil
}
}

// filterPaths filters the entries in a GitHub tree to find paths that
// match the given suffix.
// maxResults limits the number of results returned to first maxResults entries,
// a maxResults of -1 means no limit.
// It returns a slice of strings containing the matching paths.
// Directories are returned with a trailing slash.
func filterPaths(entries []*github.TreeEntry, path string, maxResults int) []string {
// Remove trailing slash for matching purposes, but flag whether we
// only want directories.
dirOnly := false
if strings.HasSuffix(path, "/") {
dirOnly = true
path = strings.TrimSuffix(path, "/")
}

matchedPaths := []string{}
for _, entry := range entries {
if len(matchedPaths) == maxResults {
break // Limit the number of results to maxResults
}
if dirOnly && entry.GetType() != "tree" {
continue // Skip non-directory entries if dirOnly is true
}
entryPath := entry.GetPath()
if entryPath == "" {
continue // Skip empty paths
}
if strings.HasSuffix(entryPath, path) {
if entry.GetType() == "tree" {
entryPath += "/" // Return directories with a trailing slash
}
matchedPaths = append(matchedPaths, entryPath)
}
}
return matchedPaths
}

// resolveGitReference resolves git references with the following logic:
// 1. If SHA is provided, it takes precedence
// 2. If neither is provided, use the default branch as ref
// 3. Get commit SHA from the ref
// Refs can look like `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`
// The function returns the resolved ref, commit SHA and any error.
func resolveGitReference(ctx context.Context, githubClient *github.Client, owner, repo, ref, sha string) (*raw.ContentOpts, error) {
// 1. If SHA is provided, use it directly
if sha != "" {
return &raw.ContentOpts{Ref: "", SHA: sha}, nil
}

// 2. If neither provided, use the default branch as ref
if ref == "" {
repoInfo, resp, err := githubClient.Repositories.Get(ctx, owner, repo)
if err != nil {
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get repository info", resp, err)
return nil, fmt.Errorf("failed to get repository info: %w", err)
}
ref = fmt.Sprintf("refs/heads/%s", repoInfo.GetDefaultBranch())
}

// 3. Get the SHA from the ref
reference, resp, err := githubClient.Git.GetRef(ctx, owner, repo, ref)
if err != nil {
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get reference", resp, err)
return nil, fmt.Errorf("failed to get reference: %w", err)
}
sha = reference.GetObject().GetSHA()

// Use provided ref, or it will be empty which defaults to the default branch
return &raw.ContentOpts{Ref: ref, SHA: sha}, nil
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp