Skip to content

Commit da49890

Browse files
committed
Addressing review comments
1 parent fc993d4 commit da49890

File tree

2 files changed

+333
-317
lines changed

2 files changed

+333
-317
lines changed

pkg/github/repositories.go

Lines changed: 4 additions & 317 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
ghErrors "github.com/github/github-mcp-server/pkg/errors"
1313
"github.com/github/github-mcp-server/pkg/inventory"
1414
"github.com/github/github-mcp-server/pkg/octicons"
15-
"github.com/github/github-mcp-server/pkg/raw"
1615
"github.com/github/github-mcp-server/pkg/translations"
1716
"github.com/github/github-mcp-server/pkg/utils"
1817
"github.com/google/go-github/v79/github"
@@ -1327,21 +1326,20 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
13271326
defer func() { _ = resp.Body.Close() }()
13281327
}
13291328
} else {
1329+
var base *github.Commit
13301330
// Repository is empty, need to initialize it first
1331-
defaultRef, base, err := initializeRepository(ctx, client, owner, repo)
1331+
ref, base, err = initializeRepository(ctx, client, owner, repo)
13321332
if err != nil {
13331333
return utils.NewToolResultError(fmt.Sprintf("failed to initialize repository: %v", err)), nil, nil
13341334
}
13351335

1336-
defaultBranch := strings.TrimPrefix(*defaultRef.Ref, "refs/heads/")
1336+
defaultBranch := strings.TrimPrefix(*ref.Ref, "refs/heads/")
13371337
if branch != defaultBranch {
13381338
// Create the requested branch from the default branch
13391339
ref, err = createReferenceFromDefaultBranch(ctx, client, owner, repo, branch)
13401340
if err != nil {
13411341
return utils.NewToolResultError(fmt.Sprintf("failed to create branch from default: %v", err)), nil, nil
13421342
}
1343-
} else {
1344-
ref = defaultRef
13451343
}
13461344

13471345
baseCommit = base
@@ -1407,6 +1405,7 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
14071405
}
14081406

14091407
// Update the reference to point to the new commit
1408+
fmt.Printf("=================== ref: %v", ref)
14101409
ref.Object.SHA = newCommit.SHA
14111410
updatedRef, resp, err := client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{
14121411
SHA: *newCommit.SHA,
@@ -1431,80 +1430,6 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
14311430
)
14321431
}
14331432

1434-
func initializeRepository(ctx context.Context, client *github.Client, owner, repo string) (ref *github.Reference, baseCommit *github.Commit, err error) {
1435-
// First, we need to check what's the default branch in this empty repo should be:
1436-
repository, resp, err := client.Repositories.Get(ctx, owner, repo)
1437-
if err != nil {
1438-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get repository", resp, err)
1439-
return nil, nil, fmt.Errorf("failed to get repository: %w", err)
1440-
}
1441-
if resp != nil && resp.Body != nil {
1442-
defer func() { _ = resp.Body.Close() }()
1443-
}
1444-
1445-
defaultBranch := repository.GetDefaultBranch()
1446-
1447-
fileOpts := &github.RepositoryContentFileOptions{
1448-
Message: github.Ptr("Initial commit"),
1449-
Content: []byte(""),
1450-
Branch: github.Ptr(defaultBranch),
1451-
}
1452-
1453-
// Create an initial empty commit to create the default branch
1454-
createResp, resp, err := client.Repositories.CreateFile(ctx, owner, repo, "README.md", fileOpts)
1455-
if err != nil {
1456-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to create initial file", resp, err)
1457-
return nil, nil, fmt.Errorf("failed to create initial file: %w", err)
1458-
}
1459-
if resp != nil && resp.Body != nil {
1460-
defer func() { _ = resp.Body.Close() }()
1461-
}
1462-
1463-
// Get the commit that was just created to use as base for remaining files
1464-
baseCommit, resp, err = client.Git.GetCommit(ctx, owner, repo, *createResp.Commit.SHA)
1465-
if err != nil {
1466-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get initial commit", resp, err)
1467-
return nil, nil, fmt.Errorf("failed to get initial commit: %w", err)
1468-
}
1469-
if resp != nil && resp.Body != nil {
1470-
defer func() { _ = resp.Body.Close() }()
1471-
}
1472-
1473-
ref, resp, err = client.Git.GetRef(ctx, owner, repo, "refs/heads/"+defaultBranch)
1474-
if err != nil {
1475-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get final reference", resp, err)
1476-
return nil, nil, fmt.Errorf("failed to get branch reference after initial commit: %w", err)
1477-
}
1478-
if resp != nil && resp.Body != nil {
1479-
defer func() { _ = resp.Body.Close() }()
1480-
}
1481-
1482-
return ref, baseCommit, nil
1483-
}
1484-
1485-
func createReferenceFromDefaultBranch(ctx context.Context, client *github.Client, owner, repo, branch string) (*github.Reference, error) {
1486-
defaultRef, err := resolveDefaultBranch(ctx, client, owner, repo)
1487-
if err != nil {
1488-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to resolve default branch", nil, err)
1489-
return nil, fmt.Errorf("failed to resolve default branch: %w", err)
1490-
}
1491-
1492-
// Create the new branch reference
1493-
createdRef, resp, err := client.Git.CreateRef(ctx, owner, repo, github.CreateRef{
1494-
Ref: "refs/heads/" + branch,
1495-
SHA: *defaultRef.Object.SHA,
1496-
})
1497-
if err != nil {
1498-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to create new branch reference", resp, err)
1499-
return nil, fmt.Errorf("failed to create new branch reference: %w", err)
1500-
}
1501-
if resp != nil && resp.Body != nil {
1502-
defer func() { _ = resp.Body.Close() }()
1503-
}
1504-
1505-
return createdRef, nil
1506-
}
1507-
15081433
// ListTags creates a tool to list tags in a GitHub repository.
15091434
func ListTags(t translations.TranslationHelperFunc) inventory.ServerTool {
15101435
return NewTool(
@@ -1895,244 +1820,6 @@ func GetReleaseByTag(t translations.TranslationHelperFunc) inventory.ServerTool
18951820
)
18961821
}
18971822

1898-
// matchFiles searches for files in the Git tree that match the given path.
1899-
// It's used when GetContents fails or returns unexpected results.
1900-
func matchFiles(ctx context.Context, client *github.Client, owner, repo, ref, path string, rawOpts *raw.ContentOpts, rawAPIResponseCode int) (*mcp.CallToolResult, any, error) {
1901-
// Step 1: Get Git Tree recursively
1902-
tree, response, err := client.Git.GetTree(ctx, owner, repo, ref, true)
1903-
if err != nil {
1904-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1905-
"failed to get git tree",
1906-
response,
1907-
err,
1908-
), nil, nil
1909-
}
1910-
defer func() { _ = response.Body.Close() }()
1911-
1912-
// Step 2: Filter tree for matching paths
1913-
const maxMatchingFiles = 3
1914-
matchingFiles := filterPaths(tree.Entries, path, maxMatchingFiles)
1915-
if len(matchingFiles) > 0 {
1916-
matchingFilesJSON, err := json.Marshal(matchingFiles)
1917-
if err != nil {
1918-
return utils.NewToolResultError(fmt.Sprintf("failed to marshal matching files: %s", err)), nil, nil
1919-
}
1920-
resolvedRefs, err := json.Marshal(rawOpts)
1921-
if err != nil {
1922-
return utils.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil, nil
1923-
}
1924-
if rawAPIResponseCode > 0 {
1925-
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
1926-
}
1927-
return utils.NewToolResultText(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s).", string(resolvedRefs), string(matchingFilesJSON))), nil, nil
1928-
}
1929-
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
1930-
}
1931-
1932-
// filterPaths filters the entries in a GitHub tree to find paths that
1933-
// match the given suffix.
1934-
// maxResults limits the number of results returned to first maxResults entries,
1935-
// a maxResults of -1 means no limit.
1936-
// It returns a slice of strings containing the matching paths.
1937-
// Directories are returned with a trailing slash.
1938-
func filterPaths(entries []*github.TreeEntry, path string, maxResults int) []string {
1939-
// Remove trailing slash for matching purposes, but flag whether we
1940-
// only want directories.
1941-
dirOnly := false
1942-
if strings.HasSuffix(path, "/") {
1943-
dirOnly = true
1944-
path = strings.TrimSuffix(path, "/")
1945-
}
1946-
1947-
matchedPaths := []string{}
1948-
for _, entry := range entries {
1949-
if len(matchedPaths) == maxResults {
1950-
break // Limit the number of results to maxResults
1951-
}
1952-
if dirOnly && entry.GetType() != "tree" {
1953-
continue // Skip non-directory entries if dirOnly is true
1954-
}
1955-
entryPath := entry.GetPath()
1956-
if entryPath == "" {
1957-
continue // Skip empty paths
1958-
}
1959-
if strings.HasSuffix(entryPath, path) {
1960-
if entry.GetType() == "tree" {
1961-
entryPath += "/" // Return directories with a trailing slash
1962-
}
1963-
matchedPaths = append(matchedPaths, entryPath)
1964-
}
1965-
}
1966-
return matchedPaths
1967-
}
1968-
1969-
// looksLikeSHA returns true if the string appears to be a Git commit SHA.
1970-
// A SHA is a 40-character hexadecimal string.
1971-
func looksLikeSHA(s string) bool {
1972-
if len(s) != 40 {
1973-
return false
1974-
}
1975-
for _, c := range s {
1976-
if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
1977-
return false
1978-
}
1979-
}
1980-
return true
1981-
}
1982-
1983-
// resolveGitReference takes a user-provided ref and sha and resolves them into a
1984-
// definitive commit SHA and its corresponding fully-qualified reference.
1985-
//
1986-
// The resolution logic follows a clear priority:
1987-
//
1988-
// 1. If a specific commit `sha` is provided, it takes precedence and is used directly,
1989-
// and all reference resolution is skipped.
1990-
//
1991-
// 1a. If `sha` is empty but `ref` looks like a commit SHA (40 hexadecimal characters),
1992-
// it is returned as-is without any API calls or reference resolution.
1993-
//
1994-
// 2. If no `sha` is provided and `ref` does not look like a SHA, the function resolves
1995-
// the `ref` string into a fully-qualified format (e.g., "refs/heads/main") by trying
1996-
// the following steps in order:
1997-
// a). **Empty Ref:** If `ref` is empty, the repository's default branch is used.
1998-
// b). **Fully-Qualified:** If `ref` already starts with "refs/", it's considered fully
1999-
// qualified and used as-is.
2000-
// c). **Partially-Qualified:** If `ref` starts with "heads/" or "tags/", it is
2001-
// prefixed with "refs/" to make it fully-qualified.
2002-
// d). **Short Name:** Otherwise, the `ref` is treated as a short name. The function
2003-
// first attempts to resolve it as a branch ("refs/heads/<ref>"). If that
2004-
// returns a 404 Not Found error, it then attempts to resolve it as a tag
2005-
// ("refs/tags/<ref>").
2006-
//
2007-
// 3. **Final Lookup:** Once a fully-qualified ref is determined, a final API call
2008-
// is made to fetch that reference's definitive commit SHA.
2009-
//
2010-
// Any unexpected (non-404) errors during the resolution process are returned
2011-
// immediately. All API errors are logged with rich context to aid diagnostics.
2012-
func resolveGitReference(ctx context.Context, githubClient *github.Client, owner, repo, ref, sha string) (*raw.ContentOpts, bool, error) {
2013-
// 1) If SHA explicitly provided, it's the highest priority.
2014-
if sha != "" {
2015-
return &raw.ContentOpts{Ref: "", SHA: sha}, false, nil
2016-
}
2017-
2018-
// 1a) If sha is empty but ref looks like a SHA, return it without changes
2019-
if looksLikeSHA(ref) {
2020-
return &raw.ContentOpts{Ref: "", SHA: ref}, false, nil
2021-
}
2022-
2023-
originalRef := ref // Keep original ref for clearer error messages down the line.
2024-
2025-
// 2) If no SHA is provided, we try to resolve the ref into a fully-qualified format.
2026-
var reference *github.Reference
2027-
var resp *github.Response
2028-
var err error
2029-
var fallbackUsed bool
2030-
2031-
switch {
2032-
case originalRef == "":
2033-
// 2a) If ref is empty, determine the default branch.
2034-
reference, err = resolveDefaultBranch(ctx, githubClient, owner, repo)
2035-
if err != nil {
2036-
return nil, false, err // Error is already wrapped in resolveDefaultBranch.
2037-
}
2038-
ref = reference.GetRef()
2039-
case strings.HasPrefix(originalRef, "refs/"):
2040-
// 2b) Already fully qualified. The reference will be fetched at the end.
2041-
case strings.HasPrefix(originalRef, "heads/") || strings.HasPrefix(originalRef, "tags/"):
2042-
// 2c) Partially qualified. Make it fully qualified.
2043-
ref = "refs/" + originalRef
2044-
default:
2045-
// 2d) It's a short name, so we try to resolve it to either a branch or a tag.
2046-
branchRef := "refs/heads/" + originalRef
2047-
reference, resp, err = githubClient.Git.GetRef(ctx, owner, repo, branchRef)
2048-
2049-
if err == nil {
2050-
ref = branchRef // It's a branch.
2051-
} else {
2052-
// The branch lookup failed. Check if it was a 404 Not Found error.
2053-
ghErr, isGhErr := err.(*github.ErrorResponse)
2054-
if isGhErr && ghErr.Response.StatusCode == http.StatusNotFound {
2055-
tagRef := "refs/tags/" + originalRef
2056-
reference, resp, err = githubClient.Git.GetRef(ctx, owner, repo, tagRef)
2057-
if err == nil {
2058-
ref = tagRef // It's a tag.
2059-
} else {
2060-
// The tag lookup also failed. Check if it was a 404 Not Found error.
2061-
ghErr2, isGhErr2 := err.(*github.ErrorResponse)
2062-
if isGhErr2 && ghErr2.Response.StatusCode == http.StatusNotFound {
2063-
if originalRef == "main" {
2064-
reference, err = resolveDefaultBranch(ctx, githubClient, owner, repo)
2065-
if err != nil {
2066-
return nil, false, err // Error is already wrapped in resolveDefaultBranch.
2067-
}
2068-
// Update ref to the actual default branch ref so the note can be generated
2069-
ref = reference.GetRef()
2070-
fallbackUsed = true
2071-
break
2072-
}
2073-
return nil, false, fmt.Errorf("could not resolve ref %q as a branch or a tag", originalRef)
2074-
}
2075-
2076-
// The tag lookup failed for a different reason.
2077-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get reference (tag)", resp, err)
2078-
return nil, false, fmt.Errorf("failed to get reference for tag '%s': %w", originalRef, err)
2079-
}
2080-
} else {
2081-
// The branch lookup failed for a different reason.
2082-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get reference (branch)", resp, err)
2083-
return nil, false, fmt.Errorf("failed to get reference for branch '%s': %w", originalRef, err)
2084-
}
2085-
}
2086-
}
2087-
2088-
if reference == nil {
2089-
reference, resp, err = githubClient.Git.GetRef(ctx, owner, repo, ref)
2090-
if err != nil {
2091-
if ref == "refs/heads/main" {
2092-
reference, err = resolveDefaultBranch(ctx, githubClient, owner, repo)
2093-
if err != nil {
2094-
return nil, false, err // Error is already wrapped in resolveDefaultBranch.
2095-
}
2096-
// Update ref to the actual default branch ref so the note can be generated
2097-
ref = reference.GetRef()
2098-
fallbackUsed = true
2099-
} else {
2100-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get final reference", resp, err)
2101-
return nil, false, fmt.Errorf("failed to get final reference for %q: %w", ref, err)
2102-
}
2103-
}
2104-
}
2105-
2106-
sha = reference.GetObject().GetSHA()
2107-
return &raw.ContentOpts{Ref: ref, SHA: sha}, fallbackUsed, nil
2108-
}
2109-
2110-
func resolveDefaultBranch(ctx context.Context, githubClient *github.Client, owner, repo string) (*github.Reference, error) {
2111-
repoInfo, resp, err := githubClient.Repositories.Get(ctx, owner, repo)
2112-
if err != nil {
2113-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get repository info", resp, err)
2114-
return nil, fmt.Errorf("failed to get repository info: %w", err)
2115-
}
2116-
2117-
if resp != nil && resp.Body != nil {
2118-
_ = resp.Body.Close()
2119-
}
2120-
2121-
defaultBranch := repoInfo.GetDefaultBranch()
2122-
2123-
defaultRef, resp, err := githubClient.Git.GetRef(ctx, owner, repo, "heads/"+defaultBranch)
2124-
if err != nil {
2125-
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get default branch reference", resp, err)
2126-
return nil, fmt.Errorf("failed to get default branch reference: %w", err)
2127-
}
2128-
2129-
if resp != nil && resp.Body != nil {
2130-
defer func() { _ = resp.Body.Close() }()
2131-
}
2132-
2133-
return defaultRef, nil
2134-
}
2135-
21361823
// ListStarredRepositories creates a tool to list starred repositories for the authenticated user or a specified user.
21371824
func ListStarredRepositories(t translations.TranslationHelperFunc) inventory.ServerTool {
21381825
return NewTool(

0 commit comments

Comments
 (0)