Skip to content

Commit a48e306

Browse files
authored
Improvements & refactoring of get_file_contents (#1582)
* Improvements & refactoring of get_file_contents * Fix logical path when file or directory not found * Fix comment * Docs update * Do file matching when raw API returns error
1 parent adaa6a1 commit a48e306

File tree

3 files changed

+62
-68
lines changed

3 files changed

+62
-68
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,7 @@ The following sets of tools are available:
10921092

10931093
- **get_file_contents** - Get file or directory contents
10941094
- `owner`: Repository owner (username or organization) (string, required)
1095-
- `path`: Path to file/directory (directories must end with a slash '/') (string, optional)
1095+
- `path`: Path to file/directory (string, optional)
10961096
- `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional)
10971097
- `repo`: Repository name (string, required)
10981098
- `sha`: Accepts optional commit SHA. If specified, it will be used instead of ref (string, optional)

pkg/github/__toolsnaps__/get_file_contents.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
"path": {
1919
"type": "string",
20-
"description": "Path to file/directory (directories must end with a slash '/')",
20+
"description": "Path to file/directory",
2121
"default": "/"
2222
},
2323
"ref": {

pkg/github/repositories.go

Lines changed: 60 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
560560
},
561561
"path": {
562562
Type: "string",
563-
Description: "Path to file/directory (directories must end with a slash '/')",
563+
Description: "Path to file/directory",
564564
Default: json.RawMessage(`"/"`),
565565
},
566566
"ref": {
@@ -608,28 +608,26 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
608608
return utils.NewToolResultError(fmt.Sprintf("failed to resolve git reference: %s", err)), nil, nil
609609
}
610610

611-
// If the path is (most likely) not to be a directory, we will
612-
// first try to get the raw content from the GitHub raw content API.
611+
if rawOpts.SHA != "" {
612+
ref = rawOpts.SHA
613+
}
613614

614-
var rawAPIResponseCode int
615-
if path != "" && !strings.HasSuffix(path, "/") {
616-
// First, get file info from Contents API to retrieve SHA
617-
var fileSHA string
618-
opts := &github.RepositoryContentGetOptions{Ref: ref}
619-
fileContent, _, respContents, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
620-
if respContents != nil {
621-
defer func() { _ = respContents.Body.Close() }()
622-
}
623-
if err != nil {
624-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
625-
"failed to get file SHA",
626-
respContents,
627-
err,
628-
), nil, nil
629-
}
630-
if fileContent == nil || fileContent.SHA == nil {
631-
return utils.NewToolResultError("file content SHA is nil, if a directory was requested, path parameters should end with a trailing slash '/'"), nil, nil
632-
}
615+
var fileSHA string
616+
opts := &github.RepositoryContentGetOptions{Ref: ref}
617+
618+
// Always call GitHub Contents API first to get metadata including SHA and determine if it's a file or directory
619+
fileContent, dirContent, respContents, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
620+
if respContents != nil {
621+
defer func() { _ = respContents.Body.Close() }()
622+
}
623+
624+
// The path does not point to a file or directory.
625+
// Instead let's try to find it in the Git Tree by matching the end of the path.
626+
if err != nil || (fileContent == nil && dirContent == nil) {
627+
return matchFiles(ctx, client, owner, repo, ref, path, rawOpts, 0)
628+
}
629+
630+
if fileContent != nil && fileContent.SHA != nil {
633631
fileSHA = *fileContent.SHA
634632

635633
rawClient, err := getRawClient(ctx)
@@ -702,55 +700,19 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
702700
}
703701
return utils.NewToolResultResource("successfully downloaded binary file", result), nil, nil
704702
}
705-
rawAPIResponseCode = resp.StatusCode
706-
}
707703

708-
if rawOpts.SHA != "" {
709-
ref = rawOpts.SHA
710-
}
711-
if strings.HasSuffix(path, "/") {
712-
opts := &github.RepositoryContentGetOptions{Ref: ref}
713-
_, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
714-
if err == nil && resp.StatusCode == http.StatusOK {
715-
defer func() { _ = resp.Body.Close() }()
716-
r, err := json.Marshal(dirContent)
717-
if err != nil {
718-
return utils.NewToolResultError("failed to marshal response"), nil, nil
719-
}
720-
return utils.NewToolResultText(string(r)), nil, nil
721-
}
722-
}
723-
724-
// The path does not point to a file or directory.
725-
// Instead let's try to find it in the Git Tree by matching the end of the path.
726-
727-
// Step 1: Get Git Tree recursively
728-
tree, resp, err := client.Git.GetTree(ctx, owner, repo, ref, true)
729-
if err != nil {
730-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
731-
"failed to get git tree",
732-
resp,
733-
err,
734-
), nil, nil
735-
}
736-
defer func() { _ = resp.Body.Close() }()
737-
738-
// Step 2: Filter tree for matching paths
739-
const maxMatchingFiles = 3
740-
matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
741-
if len(matchingFiles) > 0 {
742-
matchingFilesJSON, err := json.Marshal(matchingFiles)
704+
// Raw API call failed
705+
return matchFiles(ctx, client, owner, repo, ref, path, rawOpts, resp.StatusCode)
706+
} else if dirContent != nil {
707+
// file content or file SHA is nil which means it's a directory
708+
r, err := json.Marshal(dirContent)
743709
if err != nil {
744-
return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil
710+
return utils.NewToolResultError("failed to marshal response"), nil, nil
745711
}
746-
resolvedRefs, err := json.Marshal(rawOpts)
747-
if err != nil {
748-
return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil
749-
}
750-
return utils.NewToolResultError(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the raw content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil, nil
712+
return utils.NewToolResultText(string(r)), nil, nil
751713
}
752714

753-
return utils.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, nil
715+
return utils.NewToolResultError("failed to get file contents"), nil, nil
754716
})
755717

756718
return tool, handler
@@ -2115,3 +2077,35 @@ func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFun
21152077

21162078
return tool, handler
21172079
}
2080+
2081+
func matchFiles(ctx context.Context, client *github.Client, owner, repo, ref, path string, rawOpts *raw.ContentOpts, rawAPIResponseCode int) (*mcp.CallToolResult, any, error) {
2082+
// Step 1: Get Git Tree recursively
2083+
tree, response, err := client.Git.GetTree(ctx, owner, repo, ref, true)
2084+
if err != nil {
2085+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
2086+
"failed to get git tree",
2087+
response,
2088+
err,
2089+
), nil, nil
2090+
}
2091+
defer func() { _ = response.Body.Close() }()
2092+
2093+
// Step 2: Filter tree for matching paths
2094+
const maxMatchingFiles = 3
2095+
matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
2096+
if len(matchingFiles) > 0 {
2097+
matchingFilesJSON, err := json.Marshal(matchingFiles)
2098+
if err != nil {
2099+
return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil
2100+
}
2101+
resolvedRefs, err := json.Marshal(rawOpts)
2102+
if err != nil {
2103+
return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil
2104+
}
2105+
if rawAPIResponseCode > 0 {
2106+
return utils.NewToolResultText(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil, nil
2107+
}
2108+
return utils.NewToolResultText(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s).", string(resolvedRefs), string(matchingFilesJSON))), nil, nil
2109+
}
2110+
return utils.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, nil
2111+
}

0 commit comments

Comments
 (0)