Skip to content

Commit c68cf5b

Browse files
committed
fix: retry repository ref updates after concurrent pushes
1 parent 8b3538f commit c68cf5b

4 files changed

Lines changed: 555 additions & 223 deletions

File tree

‎pkg/github/repositories.go‎

Lines changed: 59 additions & 223 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,31 +1371,8 @@ func DeleteFile(t translations.TranslationHelperFunc) inventory.ServerTool {
13711371
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
13721372
}
13731373

1374-
// Get the reference for the branch
1375-
ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch)
1376-
if err != nil {
1377-
return nil, nil, fmt.Errorf("failed to get branch reference: %w", err)
1378-
}
1379-
defer func() { _ = resp.Body.Close() }()
1380-
1381-
// Get the commit object that the branch points to
1382-
baseCommit, resp, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA)
1383-
if err != nil {
1384-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1385-
"failed to get base commit",
1386-
resp,
1387-
err,
1388-
), nil, nil
1389-
}
1390-
defer func() { _ = resp.Body.Close() }()
1391-
1392-
if resp.StatusCode != http.StatusOK {
1393-
body, err := io.ReadAll(resp.Body)
1394-
if err != nil {
1395-
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
1396-
}
1397-
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get commit", resp, body), nil, nil
1398-
}
1374+
path = strings.TrimPrefix(path, "/")
1375+
refName := "refs/heads/" + branch
13991376

14001377
// Create a tree entry for the file deletion by setting SHA to nil
14011378
treeEntries := []*github.TreeEntry{
@@ -1407,75 +1384,29 @@ func DeleteFile(t translations.TranslationHelperFunc) inventory.ServerTool {
14071384
},
14081385
}
14091386

1410-
// Create a new tree with the deletion
1411-
newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, treeEntries)
1412-
if err != nil {
1413-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1414-
"failed to create tree",
1415-
resp,
1416-
err,
1417-
), nil, nil
1418-
}
1419-
defer func() { _ = resp.Body.Close() }()
1420-
1421-
if resp.StatusCode != http.StatusCreated {
1422-
body, err := io.ReadAll(resp.Body)
1423-
if err != nil {
1424-
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
1425-
}
1426-
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create tree", resp, body), nil, nil
1427-
}
1428-
1429-
// Create a new commit with the new tree
1430-
commit := github.Commit{
1431-
Message: github.Ptr(message),
1432-
Tree: newTree,
1433-
Parents: []*github.Commit{{SHA: baseCommit.SHA}},
1434-
}
1435-
newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil)
1387+
pushResult, resp, err := commitEntriesToRef(ctx, client, owner, repo, refName, message, treeEntries)
14361388
if err != nil {
1437-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1438-
"failed to create commit",
1439-
resp,
1440-
err,
1441-
), nil, nil
1442-
}
1443-
defer func() { _ = resp.Body.Close() }()
1444-
1445-
if resp.StatusCode != http.StatusCreated {
1446-
body, err := io.ReadAll(resp.Body)
1447-
if err != nil {
1448-
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
1389+
errMsg := err.Error()
1390+
if strings.Contains(errMsg, "get branch reference") {
1391+
return nil, nil, err
14491392
}
1450-
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create commit", resp, body), nil, nil
1451-
}
1452-
1453-
// Update the branch reference to point to the new commit
1454-
ref.Object.SHA = newCommit.SHA
1455-
_, resp, err = client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{
1456-
SHA: *newCommit.SHA,
1457-
Force: github.Ptr(false),
1458-
})
1459-
if err != nil {
1460-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1461-
"failed to update reference",
1462-
resp,
1463-
err,
1464-
), nil, nil
1465-
}
1466-
defer func() { _ = resp.Body.Close() }()
1467-
1468-
if resp.StatusCode != http.StatusOK {
1469-
body, err := io.ReadAll(resp.Body)
1470-
if err != nil {
1471-
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
1393+
stage := "failed to delete file"
1394+
switch {
1395+
case strings.Contains(errMsg, "get base commit"):
1396+
stage = "failed to get base commit"
1397+
case strings.Contains(errMsg, "create tree"):
1398+
stage = "failed to create tree"
1399+
case strings.Contains(errMsg, "create commit"):
1400+
stage = "failed to create commit"
1401+
case strings.Contains(errMsg, "update reference"):
1402+
stage = "failed to update reference"
14721403
}
1473-
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to update reference", resp, body), nil, nil
1404+
return ghErrors.NewGitHubAPIErrorResponse(ctx, stage, resp, err), nil, nil
14741405
}
14751406

14761407
// Create a response similar to what the DeleteFile API would return
14771408
response := map[string]any{
1478-
"commit": newCommit,
1409+
"commit": pushResult.Commit,
14791410
"content": nil,
14801411
}
14811412

@@ -1680,161 +1611,66 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
16801611
return utils.NewToolResultError(err.Error()), nil, nil
16811612
}
16821613

1683-
// Parse files parameter - this should be an array of objects with path and content
1684-
filesObj, ok := args["files"].([]any)
1685-
if !ok {
1686-
return utils.NewToolResultError("files parameter must be an array of objects with path and content"), nil, nil
1614+
// Parse files parameter - accept several encodings from different MCP hosts.
1615+
fileEntries, err := parsePushFilesEntries(args["files"])
1616+
if err != nil {
1617+
return utils.NewToolResultError(err.Error()), nil, nil
16871618
}
1688-
1689-
entries := make([]*github.TreeEntry, 0, len(filesObj))
1690-
for _, file := range filesObj {
1691-
fileMap, ok := file.(map[string]any)
1692-
if !ok {
1693-
return utils.NewToolResultError("each file must be an object with path and content"), nil, nil
1694-
}
1695-
1696-
filePath, ok := fileMap["path"].(string)
1697-
if !ok || filePath == "" {
1698-
return utils.NewToolResultError("each file must have a path"), nil, nil
1699-
}
1700-
filePath, err = validateRelativePath(filePath)
1619+
entries := pushFileEntriesToTreeEntries(fileEntries)
1620+
for _, entry := range entries {
1621+
filePath, err := validateRelativePath(entry.GetPath())
17011622
if err != nil {
17021623
return utils.NewToolResultError(fmt.Sprintf("invalid file path: %s", err)), nil, nil
17031624
}
1704-
1705-
content, ok := fileMap["content"].(string)
1706-
if !ok {
1707-
return utils.NewToolResultError("each file must have content"), nil, nil
1708-
}
1709-
1710-
entries = append(entries, &github.TreeEntry{
1711-
Path: github.Ptr(filePath),
1712-
Mode: github.Ptr("100644"),
1713-
Type: github.Ptr("blob"),
1714-
Content: github.Ptr(content),
1715-
})
1625+
entry.Path = github.Ptr(filePath)
17161626
}
17171627

17181628
client, err := deps.GetClient(ctx)
17191629
if err != nil {
17201630
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
17211631
}
17221632

1723-
// Get the reference for the branch
1724-
var repositoryIsEmpty bool
1725-
var branchNotFound bool
1726-
ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch)
1633+
refName, err := ensurePushFilesBranchRef(ctx, client, owner, repo, branch)
17271634
if err != nil {
1728-
ghErr, isGhErr := err.(*github.ErrorResponse)
1729-
if isGhErr {
1730-
if ghErr.Response.StatusCode == http.StatusConflict && ghErr.Message == "Git Repository is empty." {
1731-
repositoryIsEmpty = true
1732-
} else if ghErr.Response.StatusCode == http.StatusNotFound {
1733-
branchNotFound = true
1734-
}
1735-
}
1736-
1737-
if !repositoryIsEmpty && !branchNotFound {
1738-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1739-
"failed to get branch reference",
1740-
resp,
1741-
err,
1742-
), nil, nil
1743-
}
1744-
}
1745-
// Only close resp if it's not nil and not an error case where resp might be nil
1746-
if resp != nil && resp.Body != nil {
1747-
defer func() { _ = resp.Body.Close() }()
1748-
}
1749-
1750-
var baseCommit *github.Commit
1751-
if !repositoryIsEmpty {
1752-
if branchNotFound {
1753-
ref, err = createReferenceFromDefaultBranch(ctx, client, owner, repo, branch)
1754-
if err != nil {
1755-
return utils.NewToolResultError(fmt.Sprintf("failed to create branch from default: %v", err)), nil, nil
1756-
}
1757-
}
1758-
1759-
// Get the commit object that the branch points to
1760-
baseCommit, resp, err = client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA)
1761-
if err != nil {
1762-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1763-
"failed to get base commit",
1764-
resp,
1765-
err,
1766-
), nil, nil
1767-
}
1768-
if resp != nil && resp.Body != nil {
1769-
defer func() { _ = resp.Body.Close() }()
1770-
}
1771-
} else {
1772-
var base *github.Commit
1773-
// Repository is empty, need to initialize it first
1774-
ref, base, err = initializeRepository(ctx, client, owner, repo)
1775-
if err != nil {
1635+
errMsg := err.Error()
1636+
switch {
1637+
case strings.Contains(errMsg, "initialize repository"):
17761638
return utils.NewToolResultError(fmt.Sprintf("failed to initialize repository: %v", err)), nil, nil
1639+
case strings.Contains(errMsg, "create branch from default"):
1640+
return utils.NewToolResultError(fmt.Sprintf("failed to create branch from default: %v", err)), nil, nil
1641+
default:
1642+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get branch reference", nil, err), nil, nil
17771643
}
1778-
1779-
defaultBranch := strings.TrimPrefix(*ref.Ref, "refs/heads/")
1780-
if branch != defaultBranch {
1781-
// Create the requested branch from the default branch
1782-
ref, err = createReferenceFromDefaultBranch(ctx, client, owner, repo, branch)
1783-
if err != nil {
1784-
return utils.NewToolResultError(fmt.Sprintf("failed to create branch from default: %v", err)), nil, nil
1785-
}
1786-
}
1787-
1788-
baseCommit = base
17891644
}
17901645

1791-
// Create a new tree with the file entries (baseCommit is now guaranteed to exist)
1792-
newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, entries)
1793-
if err != nil {
1794-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1795-
"failed to create tree",
1796-
resp,
1797-
err,
1798-
), nil, nil
1799-
}
1800-
if resp != nil && resp.Body != nil {
1801-
defer func() { _ = resp.Body.Close() }()
1802-
}
1803-
1804-
// Create a new commit (baseCommit always has a value now)
1805-
commit := github.Commit{
1806-
Message: github.Ptr(message),
1807-
Tree: newTree,
1808-
Parents: []*github.Commit{{SHA: baseCommit.SHA}},
1809-
}
1810-
newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil)
1646+
pushResult, resp, err := commitEntriesToRef(
1647+
ctx,
1648+
client,
1649+
owner,
1650+
repo,
1651+
refName,
1652+
message,
1653+
entries,
1654+
)
18111655
if err != nil {
1812-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1813-
"failed to create commit",
1814-
resp,
1815-
err,
1816-
), nil, nil
1817-
}
1818-
if resp != nil && resp.Body != nil {
1819-
defer func() { _ = resp.Body.Close() }()
1820-
}
1821-
1822-
// Update the reference to point to the new commit
1823-
ref.Object.SHA = newCommit.SHA
1824-
updatedRef, resp, err := client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{
1825-
SHA: *newCommit.SHA,
1826-
Force: github.Ptr(false),
1827-
})
1828-
if err != nil {
1829-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1830-
"failed to update reference",
1831-
resp,
1832-
err,
1833-
), nil, nil
1656+
stage := "failed to push files"
1657+
errMsg := err.Error()
1658+
switch {
1659+
case strings.Contains(errMsg, "get base commit"):
1660+
stage = "failed to get base commit"
1661+
case strings.Contains(errMsg, "create tree"):
1662+
stage = "failed to create tree"
1663+
case strings.Contains(errMsg, "create commit"):
1664+
stage = "failed to create commit"
1665+
case strings.Contains(errMsg, "update reference"):
1666+
stage = "failed to update reference"
1667+
case strings.Contains(errMsg, "get branch reference"):
1668+
stage = "failed to get branch reference"
1669+
}
1670+
return ghErrors.NewGitHubAPIErrorResponse(ctx, stage, resp, err), nil, nil
18341671
}
1835-
defer func() { _ = resp.Body.Close() }()
18361672

1837-
r, err := json.Marshal(updatedRef)
1673+
r, err := json.Marshal(pushResult.Ref)
18381674
if err != nil {
18391675
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
18401676
}

0 commit comments

Comments
 (0)