@@ -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"
@@ -1279,28 +1278,74 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
12791278 }
12801279
12811280 // Get the reference for the branch
1281+ var repositoryIsEmpty bool
1282+ var branchNotFound bool
12821283 ref , resp , err := client .Git .GetRef (ctx , owner , repo , "refs/heads/" + branch )
12831284 if err != nil {
1284- return ghErrors .NewGitHubAPIErrorResponse (ctx ,
1285- "failed to get branch reference" ,
1286- resp ,
1287- err ,
1288- ), nil , nil
1285+ ghErr , isGhErr := err .(* github.ErrorResponse )
1286+ if isGhErr {
1287+ if ghErr .Response .StatusCode == http .StatusConflict && ghErr .Message == "Git Repository is empty." {
1288+ repositoryIsEmpty = true
1289+ } else if ghErr .Response .StatusCode == http .StatusNotFound {
1290+ branchNotFound = true
1291+ }
1292+ }
1293+
1294+ if ! repositoryIsEmpty && ! branchNotFound {
1295+ return ghErrors .NewGitHubAPIErrorResponse (ctx ,
1296+ "failed to get branch reference" ,
1297+ resp ,
1298+ err ,
1299+ ), nil , nil
1300+ }
1301+ }
1302+ // Only close resp if it's not nil and not an error case where resp might be nil
1303+ if resp != nil && resp .Body != nil {
1304+ defer func () { _ = resp .Body .Close () }()
12891305 }
1290- defer func () { _ = resp .Body .Close () }()
12911306
1292- // Get the commit object that the branch points to
1293- baseCommit , resp , err := client .Git .GetCommit (ctx , owner , repo , * ref .Object .SHA )
1294- if err != nil {
1295- return ghErrors .NewGitHubAPIErrorResponse (ctx ,
1296- "failed to get base commit" ,
1297- resp ,
1298- err ,
1299- ), nil , nil
1307+ var baseCommit * github.Commit
1308+ if ! repositoryIsEmpty {
1309+ if branchNotFound {
1310+ ref , err = createReferenceFromDefaultBranch (ctx , client , owner , repo , branch )
1311+ if err != nil {
1312+ return utils .NewToolResultError (fmt .Sprintf ("failed to create branch from default: %v" , err )), nil , nil
1313+ }
1314+ }
1315+
1316+ // Get the commit object that the branch points to
1317+ baseCommit , resp , err = client .Git .GetCommit (ctx , owner , repo , * ref .Object .SHA )
1318+ if err != nil {
1319+ return ghErrors .NewGitHubAPIErrorResponse (ctx ,
1320+ "failed to get base commit" ,
1321+ resp ,
1322+ err ,
1323+ ), nil , nil
1324+ }
1325+ if resp != nil && resp .Body != nil {
1326+ defer func () { _ = resp .Body .Close () }()
1327+ }
1328+ } else {
1329+ var base * github.Commit
1330+ // Repository is empty, need to initialize it first
1331+ ref , base , err = initializeRepository (ctx , client , owner , repo )
1332+ if err != nil {
1333+ return utils .NewToolResultError (fmt .Sprintf ("failed to initialize repository: %v" , err )), nil , nil
1334+ }
1335+
1336+ defaultBranch := strings .TrimPrefix (* ref .Ref , "refs/heads/" )
1337+ if branch != defaultBranch {
1338+ // Create the requested branch from the default branch
1339+ ref , err = createReferenceFromDefaultBranch (ctx , client , owner , repo , branch )
1340+ if err != nil {
1341+ return utils .NewToolResultError (fmt .Sprintf ("failed to create branch from default: %v" , err )), nil , nil
1342+ }
1343+ }
1344+
1345+ baseCommit = base
13001346 }
1301- defer func () { _ = resp .Body .Close () }()
13021347
1303- // Create tree entries for all files
1348+ // Create tree entries for all files (or remaining files if empty repo)
13041349 var entries []* github.TreeEntry
13051350
13061351 for _ , file := range filesObj {
@@ -1328,7 +1373,7 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
13281373 })
13291374 }
13301375
1331- // Create a new tree with the file entries
1376+ // Create a new tree with the file entries (baseCommit is now guaranteed to exist)
13321377 newTree , resp , err := client .Git .CreateTree (ctx , owner , repo , * baseCommit .Tree .SHA , entries )
13331378 if err != nil {
13341379 return ghErrors .NewGitHubAPIErrorResponse (ctx ,
@@ -1337,9 +1382,11 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
13371382 err ,
13381383 ), nil , nil
13391384 }
1340- defer func () { _ = resp .Body .Close () }()
1385+ if resp != nil && resp .Body != nil {
1386+ defer func () { _ = resp .Body .Close () }()
1387+ }
13411388
1342- // Create a new commit
1389+ // Create a new commit (baseCommit always has a value now)
13431390 commit := github.Commit {
13441391 Message : github .Ptr (message ),
13451392 Tree : newTree ,
@@ -1353,7 +1400,9 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
13531400 err ,
13541401 ), nil , nil
13551402 }
1356- defer func () { _ = resp .Body .Close () }()
1403+ if resp != nil && resp .Body != nil {
1404+ defer func () { _ = resp .Body .Close () }()
1405+ }
13571406
13581407 // Update the reference to point to the new commit
13591408 ref .Object .SHA = newCommit .SHA
@@ -1770,244 +1819,6 @@ func GetReleaseByTag(t translations.TranslationHelperFunc) inventory.ServerTool
17701819 )
17711820}
17721821
1773- // matchFiles searches for files in the Git tree that match the given path.
1774- // It's used when GetContents fails or returns unexpected results.
1775- func matchFiles (ctx context.Context , client * github.Client , owner , repo , ref , path string , rawOpts * raw.ContentOpts , rawAPIResponseCode int ) (* mcp.CallToolResult , any , error ) {
1776- // Step 1: Get Git Tree recursively
1777- tree , response , err := client .Git .GetTree (ctx , owner , repo , ref , true )
1778- if err != nil {
1779- return ghErrors .NewGitHubAPIErrorResponse (ctx ,
1780- "failed to get git tree" ,
1781- response ,
1782- err ,
1783- ), nil , nil
1784- }
1785- defer func () { _ = response .Body .Close () }()
1786-
1787- // Step 2: Filter tree for matching paths
1788- const maxMatchingFiles = 3
1789- matchingFiles := filterPaths (tree .Entries , path , maxMatchingFiles )
1790- if len (matchingFiles ) > 0 {
1791- matchingFilesJSON , err := json .Marshal (matchingFiles )
1792- if err != nil {
1793- return utils .NewToolResultError (fmt .Sprintf ("failed to marshal matching files: %s" , err )), nil , nil
1794- }
1795- resolvedRefs , err := json .Marshal (rawOpts )
1796- if err != nil {
1797- return utils .NewToolResultError (fmt .Sprintf ("failed to marshal resolved refs: %s" , err )), nil , nil
1798- }
1799- if rawAPIResponseCode > 0 {
1800- 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
1801- }
1802- return utils .NewToolResultText (fmt .Sprintf ("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s)." , string (resolvedRefs ), string (matchingFilesJSON ))), nil , nil
1803- }
1804- 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
1805- }
1806-
1807- // filterPaths filters the entries in a GitHub tree to find paths that
1808- // match the given suffix.
1809- // maxResults limits the number of results returned to first maxResults entries,
1810- // a maxResults of -1 means no limit.
1811- // It returns a slice of strings containing the matching paths.
1812- // Directories are returned with a trailing slash.
1813- func filterPaths (entries []* github.TreeEntry , path string , maxResults int ) []string {
1814- // Remove trailing slash for matching purposes, but flag whether we
1815- // only want directories.
1816- dirOnly := false
1817- if strings .HasSuffix (path , "/" ) {
1818- dirOnly = true
1819- path = strings .TrimSuffix (path , "/" )
1820- }
1821-
1822- matchedPaths := []string {}
1823- for _ , entry := range entries {
1824- if len (matchedPaths ) == maxResults {
1825- break // Limit the number of results to maxResults
1826- }
1827- if dirOnly && entry .GetType () != "tree" {
1828- continue // Skip non-directory entries if dirOnly is true
1829- }
1830- entryPath := entry .GetPath ()
1831- if entryPath == "" {
1832- continue // Skip empty paths
1833- }
1834- if strings .HasSuffix (entryPath , path ) {
1835- if entry .GetType () == "tree" {
1836- entryPath += "/" // Return directories with a trailing slash
1837- }
1838- matchedPaths = append (matchedPaths , entryPath )
1839- }
1840- }
1841- return matchedPaths
1842- }
1843-
1844- // looksLikeSHA returns true if the string appears to be a Git commit SHA.
1845- // A SHA is a 40-character hexadecimal string.
1846- func looksLikeSHA (s string ) bool {
1847- if len (s ) != 40 {
1848- return false
1849- }
1850- for _ , c := range s {
1851- if (c < '0' || c > '9' ) && (c < 'a' || c > 'f' ) && (c < 'A' || c > 'F' ) {
1852- return false
1853- }
1854- }
1855- return true
1856- }
1857-
1858- // resolveGitReference takes a user-provided ref and sha and resolves them into a
1859- // definitive commit SHA and its corresponding fully-qualified reference.
1860- //
1861- // The resolution logic follows a clear priority:
1862- //
1863- // 1. If a specific commit `sha` is provided, it takes precedence and is used directly,
1864- // and all reference resolution is skipped.
1865- //
1866- // 1a. If `sha` is empty but `ref` looks like a commit SHA (40 hexadecimal characters),
1867- // it is returned as-is without any API calls or reference resolution.
1868- //
1869- // 2. If no `sha` is provided and `ref` does not look like a SHA, the function resolves
1870- // the `ref` string into a fully-qualified format (e.g., "refs/heads/main") by trying
1871- // the following steps in order:
1872- // a). **Empty Ref:** If `ref` is empty, the repository's default branch is used.
1873- // b). **Fully-Qualified:** If `ref` already starts with "refs/", it's considered fully
1874- // qualified and used as-is.
1875- // c). **Partially-Qualified:** If `ref` starts with "heads/" or "tags/", it is
1876- // prefixed with "refs/" to make it fully-qualified.
1877- // d). **Short Name:** Otherwise, the `ref` is treated as a short name. The function
1878- // first attempts to resolve it as a branch ("refs/heads/<ref>"). If that
1879- // returns a 404 Not Found error, it then attempts to resolve it as a tag
1880- // ("refs/tags/<ref>").
1881- //
1882- // 3. **Final Lookup:** Once a fully-qualified ref is determined, a final API call
1883- // is made to fetch that reference's definitive commit SHA.
1884- //
1885- // Any unexpected (non-404) errors during the resolution process are returned
1886- // immediately. All API errors are logged with rich context to aid diagnostics.
1887- func resolveGitReference (ctx context.Context , githubClient * github.Client , owner , repo , ref , sha string ) (* raw.ContentOpts , bool , error ) {
1888- // 1) If SHA explicitly provided, it's the highest priority.
1889- if sha != "" {
1890- return & raw.ContentOpts {Ref : "" , SHA : sha }, false , nil
1891- }
1892-
1893- // 1a) If sha is empty but ref looks like a SHA, return it without changes
1894- if looksLikeSHA (ref ) {
1895- return & raw.ContentOpts {Ref : "" , SHA : ref }, false , nil
1896- }
1897-
1898- originalRef := ref // Keep original ref for clearer error messages down the line.
1899-
1900- // 2) If no SHA is provided, we try to resolve the ref into a fully-qualified format.
1901- var reference * github.Reference
1902- var resp * github.Response
1903- var err error
1904- var fallbackUsed bool
1905-
1906- switch {
1907- case originalRef == "" :
1908- // 2a) If ref is empty, determine the default branch.
1909- reference , err = resolveDefaultBranch (ctx , githubClient , owner , repo )
1910- if err != nil {
1911- return nil , false , err // Error is already wrapped in resolveDefaultBranch.
1912- }
1913- ref = reference .GetRef ()
1914- case strings .HasPrefix (originalRef , "refs/" ):
1915- // 2b) Already fully qualified. The reference will be fetched at the end.
1916- case strings .HasPrefix (originalRef , "heads/" ) || strings .HasPrefix (originalRef , "tags/" ):
1917- // 2c) Partially qualified. Make it fully qualified.
1918- ref = "refs/" + originalRef
1919- default :
1920- // 2d) It's a short name, so we try to resolve it to either a branch or a tag.
1921- branchRef := "refs/heads/" + originalRef
1922- reference , resp , err = githubClient .Git .GetRef (ctx , owner , repo , branchRef )
1923-
1924- if err == nil {
1925- ref = branchRef // It's a branch.
1926- } else {
1927- // The branch lookup failed. Check if it was a 404 Not Found error.
1928- ghErr , isGhErr := err .(* github.ErrorResponse )
1929- if isGhErr && ghErr .Response .StatusCode == http .StatusNotFound {
1930- tagRef := "refs/tags/" + originalRef
1931- reference , resp , err = githubClient .Git .GetRef (ctx , owner , repo , tagRef )
1932- if err == nil {
1933- ref = tagRef // It's a tag.
1934- } else {
1935- // The tag lookup also failed. Check if it was a 404 Not Found error.
1936- ghErr2 , isGhErr2 := err .(* github.ErrorResponse )
1937- if isGhErr2 && ghErr2 .Response .StatusCode == http .StatusNotFound {
1938- if originalRef == "main" {
1939- reference , err = resolveDefaultBranch (ctx , githubClient , owner , repo )
1940- if err != nil {
1941- return nil , false , err // Error is already wrapped in resolveDefaultBranch.
1942- }
1943- // Update ref to the actual default branch ref so the note can be generated
1944- ref = reference .GetRef ()
1945- fallbackUsed = true
1946- break
1947- }
1948- return nil , false , fmt .Errorf ("could not resolve ref %q as a branch or a tag" , originalRef )
1949- }
1950-
1951- // The tag lookup failed for a different reason.
1952- _ , _ = ghErrors .NewGitHubAPIErrorToCtx (ctx , "failed to get reference (tag)" , resp , err )
1953- return nil , false , fmt .Errorf ("failed to get reference for tag '%s': %w" , originalRef , err )
1954- }
1955- } else {
1956- // The branch lookup failed for a different reason.
1957- _ , _ = ghErrors .NewGitHubAPIErrorToCtx (ctx , "failed to get reference (branch)" , resp , err )
1958- return nil , false , fmt .Errorf ("failed to get reference for branch '%s': %w" , originalRef , err )
1959- }
1960- }
1961- }
1962-
1963- if reference == nil {
1964- reference , resp , err = githubClient .Git .GetRef (ctx , owner , repo , ref )
1965- if err != nil {
1966- if ref == "refs/heads/main" {
1967- reference , err = resolveDefaultBranch (ctx , githubClient , owner , repo )
1968- if err != nil {
1969- return nil , false , err // Error is already wrapped in resolveDefaultBranch.
1970- }
1971- // Update ref to the actual default branch ref so the note can be generated
1972- ref = reference .GetRef ()
1973- fallbackUsed = true
1974- } else {
1975- _ , _ = ghErrors .NewGitHubAPIErrorToCtx (ctx , "failed to get final reference" , resp , err )
1976- return nil , false , fmt .Errorf ("failed to get final reference for %q: %w" , ref , err )
1977- }
1978- }
1979- }
1980-
1981- sha = reference .GetObject ().GetSHA ()
1982- return & raw.ContentOpts {Ref : ref , SHA : sha }, fallbackUsed , nil
1983- }
1984-
1985- func resolveDefaultBranch (ctx context.Context , githubClient * github.Client , owner , repo string ) (* github.Reference , error ) {
1986- repoInfo , resp , err := githubClient .Repositories .Get (ctx , owner , repo )
1987- if err != nil {
1988- _ , _ = ghErrors .NewGitHubAPIErrorToCtx (ctx , "failed to get repository info" , resp , err )
1989- return nil , fmt .Errorf ("failed to get repository info: %w" , err )
1990- }
1991-
1992- if resp != nil && resp .Body != nil {
1993- _ = resp .Body .Close ()
1994- }
1995-
1996- defaultBranch := repoInfo .GetDefaultBranch ()
1997-
1998- defaultRef , resp , err := githubClient .Git .GetRef (ctx , owner , repo , "heads/" + defaultBranch )
1999- if err != nil {
2000- _ , _ = ghErrors .NewGitHubAPIErrorToCtx (ctx , "failed to get default branch reference" , resp , err )
2001- return nil , fmt .Errorf ("failed to get default branch reference: %w" , err )
2002- }
2003-
2004- if resp != nil && resp .Body != nil {
2005- defer func () { _ = resp .Body .Close () }()
2006- }
2007-
2008- return defaultRef , nil
2009- }
2010-
20111822// ListStarredRepositories creates a tool to list starred repositories for the authenticated user or a specified user.
20121823func ListStarredRepositories (t translations.TranslationHelperFunc ) inventory.ServerTool {
20131824 return NewTool (
0 commit comments