- Notifications
You must be signed in to change notification settings - Fork1.5k
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
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
3269af4
be2f36f
37a6088
588866f
1cc6f7c
11d8a45
12c2cfe
8c40155
bf673f8
43d72ed
3aea320
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -8,7 +8,6 @@ import ( | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
LuluBeatson marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
ghErrors "github.com/github/github-mcp-server/pkg/errors" | ||
@@ -495,33 +494,18 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t | ||
return mcp.NewToolResultError(err.Error()), nil | ||
} | ||
client, err := getClient(ctx) | ||
if err != nil { | ||
return mcp.NewToolResultError("failed to get GitHub client"), nil | ||
} | ||
LuluBeatson marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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 path != "" && !strings.HasSuffix(path, "/") { | ||
rawClient, err := getRawClient(ctx) | ||
@@ -580,36 +564,51 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t | ||
} | ||
} | ||
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 && resp.StatusCode == http.StatusOK { | ||
defer func() { _ = resp.Body.Close() }() | ||
r, err := json.Marshal(dirContent) | ||
if err != nil { | ||
return mcp.NewToolResultError("failed tomarshal response"), nil | ||
} | ||
LuluBeatson marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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() }() | ||
// 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(fmt.Sprintf("failed to marshalresolved refs: %s", err)), 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 | ||
} | ||
} | ||
@@ -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 { | ||
LuluBeatson marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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 | ||
} |
Uh oh!
There was an error while loading.Please reload this page.