Skip to content

Commit 5c98558

Browse files
Use createCommitOnBranch for file write tools
1 parent 63d313a commit 5c98558

3 files changed

Lines changed: 392 additions & 241 deletions

File tree

pkg/github/minimal_types.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -669,42 +669,6 @@ func convertToMinimalIssueComment(comment *github.IssueComment) MinimalIssueComm
669669
return m
670670
}
671671

672-
func convertToMinimalFileContentResponse(resp *github.RepositoryContentResponse) MinimalFileContentResponse {
673-
m := MinimalFileContentResponse{}
674-
675-
if resp == nil {
676-
return m
677-
}
678-
679-
if c := resp.Content; c != nil {
680-
m.Content = &MinimalFileContent{
681-
Name: c.GetName(),
682-
Path: c.GetPath(),
683-
SHA: c.GetSHA(),
684-
Size: c.GetSize(),
685-
HTMLURL: c.GetHTMLURL(),
686-
}
687-
}
688-
689-
m.Commit = &MinimalFileCommit{
690-
SHA: resp.Commit.GetSHA(),
691-
Message: resp.Commit.GetMessage(),
692-
HTMLURL: resp.Commit.GetHTMLURL(),
693-
}
694-
695-
if author := resp.Commit.Author; author != nil {
696-
m.Commit.Author = &MinimalCommitAuthor{
697-
Name: author.GetName(),
698-
Email: author.GetEmail(),
699-
}
700-
if author.Date != nil {
701-
m.Commit.Author.Date = author.Date.Format(time.RFC3339)
702-
}
703-
}
704-
705-
return m
706-
}
707-
708672
func convertToMinimalPullRequest(pr *github.PullRequest) MinimalPullRequest {
709673
m := MinimalPullRequest{
710674
Number: pr.GetNumber(),

pkg/github/repositories.go

Lines changed: 150 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,97 @@ import (
2424
"github.com/shurcooL/githubv4"
2525
)
2626

27+
type commitOnBranchFile struct {
28+
Path string
29+
Content string
30+
}
31+
32+
type commitOnBranchResult struct {
33+
SHA string
34+
Message string
35+
HTMLURL string
36+
Author *github.CommitAuthor
37+
}
38+
39+
func createCommitOnBranch(ctx context.Context, client *githubv4.Client, owner, repo, branch, message, expectedHeadOID string, files []commitOnBranchFile) (*commitOnBranchResult, error) {
40+
if client == nil {
41+
return nil, fmt.Errorf("GitHub GraphQL client is not configured")
42+
}
43+
44+
additions := make([]githubv4.FileAddition, 0, len(files))
45+
for _, file := range files {
46+
additions = append(additions, githubv4.FileAddition{
47+
Path: githubv4.String(strings.TrimPrefix(file.Path, "/")),
48+
Contents: githubv4.Base64String(base64.StdEncoding.EncodeToString([]byte(file.Content))),
49+
})
50+
}
51+
52+
var mutation struct {
53+
CreateCommitOnBranch struct {
54+
Commit struct {
55+
OID githubv4.GitObjectID `graphql:"oid"`
56+
Message githubv4.String
57+
URL githubv4.URI
58+
Author struct {
59+
Name githubv4.String
60+
Email githubv4.String
61+
Date githubv4.DateTime
62+
}
63+
}
64+
} `graphql:"createCommitOnBranch(input: $input)"`
65+
}
66+
67+
input := githubv4.CreateCommitOnBranchInput{
68+
Branch: githubv4.CommittableBranch{
69+
RepositoryNameWithOwner: githubv4.NewString(githubv4.String(owner + "/" + repo)),
70+
BranchName: githubv4.NewString(githubv4.String(branch)),
71+
},
72+
Message: githubv4.CommitMessage{
73+
Headline: githubv4.String(message),
74+
},
75+
ExpectedHeadOid: githubv4.GitObjectID(expectedHeadOID),
76+
FileChanges: &githubv4.FileChanges{
77+
Additions: &additions,
78+
},
79+
}
80+
81+
if err := client.Mutate(ctx, &mutation, input, nil); err != nil {
82+
return nil, err
83+
}
84+
85+
commit := mutation.CreateCommitOnBranch.Commit
86+
result := &commitOnBranchResult{
87+
SHA: string(commit.OID),
88+
Message: string(commit.Message),
89+
HTMLURL: commit.URL.String(),
90+
Author: &github.CommitAuthor{
91+
Name: github.Ptr(string(commit.Author.Name)),
92+
Email: github.Ptr(string(commit.Author.Email)),
93+
},
94+
}
95+
if !commit.Author.Date.Time.IsZero() {
96+
result.Author.Date = &github.Timestamp{Time: commit.Author.Date.Time}
97+
}
98+
99+
return result, nil
100+
}
101+
102+
func minimalFileCommitFromCommitOnBranchResult(commit *commitOnBranchResult) *MinimalFileCommit {
103+
if commit == nil {
104+
return nil
105+
}
106+
107+
m := &MinimalFileCommit{
108+
SHA: commit.SHA,
109+
Message: commit.Message,
110+
HTMLURL: commit.HTMLURL,
111+
}
112+
if commit.Author != nil {
113+
m.Author = convertToMinimalCommitAuthor(commit.Author)
114+
}
115+
return m
116+
}
117+
27118
func GetCommit(t translations.TranslationHelperFunc) inventory.ServerTool {
28119
return NewTool(
29120
ToolsetMetadataRepos,
@@ -456,24 +547,10 @@ SHA MUST be provided for existing file updates.
456547
return utils.NewToolResultError(err.Error()), nil, nil
457548
}
458549

459-
// json.Marshal encodes byte arrays with base64, which is required for the API.
460-
contentBytes := []byte(content)
461-
462-
// Create the file options
463-
opts := &github.RepositoryContentFileOptions{
464-
Message: github.Ptr(message),
465-
Content: contentBytes,
466-
Branch: github.Ptr(branch),
467-
}
468-
469-
// If SHA is provided, set it (for updates)
470550
sha, err := OptionalParam[string](args, "sha")
471551
if err != nil {
472552
return utils.NewToolResultError(err.Error()), nil, nil
473553
}
474-
if sha != "" {
475-
opts.SHA = github.Ptr(sha)
476-
}
477554

478555
// Create or update the file
479556
client, err := deps.GetClient(ctx)
@@ -547,25 +624,61 @@ SHA MUST be provided for existing file updates.
547624
// If file not found, no previous SHA needed (new file creation)
548625
}
549626

550-
fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, path, opts)
627+
ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch)
551628
if err != nil {
552629
return ghErrors.NewGitHubAPIErrorResponse(ctx,
553630
"failed to create/update file",
554631
resp,
555632
err,
556633
), nil, nil
557634
}
558-
defer func() { _ = resp.Body.Close() }()
635+
if resp != nil && resp.Body != nil {
636+
defer func() { _ = resp.Body.Close() }()
637+
}
638+
if ref == nil || ref.Object == nil || ref.Object.SHA == nil {
639+
return utils.NewToolResultError(fmt.Sprintf("failed to resolve branch head for %s", branch)), nil, nil
640+
}
559641

560-
if resp.StatusCode != 200 && resp.StatusCode != 201 {
561-
body, err := io.ReadAll(resp.Body)
562-
if err != nil {
563-
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
564-
}
565-
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create/update file", resp, body), nil, nil
642+
gqlClient, err := deps.GetGQLClient(ctx)
643+
if err != nil {
644+
return nil, nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err)
645+
}
646+
commit, err := createCommitOnBranch(ctx, gqlClient, owner, repo, branch, message, *ref.Object.SHA, []commitOnBranchFile{
647+
{Path: path, Content: content},
648+
})
649+
if err != nil {
650+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to create/update file", err), nil, nil
651+
}
652+
653+
updatedFile, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, &github.RepositoryContentGetOptions{Ref: commit.SHA})
654+
if err != nil {
655+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
656+
"failed to get updated file contents",
657+
resp,
658+
err,
659+
), nil, nil
660+
}
661+
if resp != nil && resp.Body != nil {
662+
defer func() { _ = resp.Body.Close() }()
663+
}
664+
if dirContent != nil {
665+
return utils.NewToolResultError(fmt.Sprintf(
666+
"Path %s is a directory, not a file. This tool only works with files.",
667+
path)), nil, nil
566668
}
567669

568-
minimalResponse := convertToMinimalFileContentResponse(fileContent)
670+
minimalResponse := MinimalFileContentResponse{
671+
Commit: minimalFileCommitFromCommitOnBranchResult(commit),
672+
}
673+
if updatedFile != nil {
674+
minimalResponse.Content = &MinimalFileContent{
675+
Name: updatedFile.GetName(),
676+
Path: updatedFile.GetPath(),
677+
SHA: updatedFile.GetSHA(),
678+
Size: updatedFile.GetSize(),
679+
HTMLURL: updatedFile.GetHTMLURL(),
680+
}
681+
}
569682

570683
return MarshalledTextResult(minimalResponse), nil, nil
571684
},
@@ -1443,8 +1556,7 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
14431556
baseCommit = base
14441557
}
14451558

1446-
// Create tree entries for all files (or remaining files if empty repo)
1447-
var entries []*github.TreeEntry
1559+
fileChanges := make([]commitOnBranchFile, 0, len(filesObj))
14481560

14491561
for _, file := range filesObj {
14501562
fileMap, ok := file.(map[string]any)
@@ -1462,60 +1574,28 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
14621574
return utils.NewToolResultError("each file must have content"), nil, nil
14631575
}
14641576

1465-
// Create a tree entry for the file
1466-
entries = append(entries, &github.TreeEntry{
1467-
Path: github.Ptr(path),
1468-
Mode: github.Ptr("100644"), // Regular file mode
1469-
Type: github.Ptr("blob"),
1470-
Content: github.Ptr(content),
1577+
fileChanges = append(fileChanges, commitOnBranchFile{
1578+
Path: path,
1579+
Content: content,
14711580
})
14721581
}
14731582

1474-
// Create a new tree with the file entries (baseCommit is now guaranteed to exist)
1475-
newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, entries)
1583+
gqlClient, err := deps.GetGQLClient(ctx)
14761584
if err != nil {
1477-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1478-
"failed to create tree",
1479-
resp,
1480-
err,
1481-
), nil, nil
1482-
}
1483-
if resp != nil && resp.Body != nil {
1484-
defer func() { _ = resp.Body.Close() }()
1485-
}
1486-
1487-
// Create a new commit (baseCommit always has a value now)
1488-
commit := github.Commit{
1489-
Message: github.Ptr(message),
1490-
Tree: newTree,
1491-
Parents: []*github.Commit{{SHA: baseCommit.SHA}},
1585+
return nil, nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err)
14921586
}
1493-
newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil)
1587+
newCommit, err := createCommitOnBranch(ctx, gqlClient, owner, repo, branch, message, *baseCommit.SHA, fileChanges)
14941588
if err != nil {
1495-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1496-
"failed to create commit",
1497-
resp,
1498-
err,
1499-
), nil, nil
1500-
}
1501-
if resp != nil && resp.Body != nil {
1502-
defer func() { _ = resp.Body.Close() }()
1589+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to create commit", err), nil, nil
15031590
}
15041591

1505-
// Update the reference to point to the new commit
1506-
ref.Object.SHA = newCommit.SHA
1507-
updatedRef, resp, err := client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{
1508-
SHA: *newCommit.SHA,
1509-
Force: github.Ptr(false),
1510-
})
1511-
if err != nil {
1512-
return ghErrors.NewGitHubAPIErrorResponse(ctx,
1513-
"failed to update reference",
1514-
resp,
1515-
err,
1516-
), nil, nil
1592+
updatedRef := &github.Reference{
1593+
Ref: ref.Ref,
1594+
Object: &github.GitObject{
1595+
SHA: github.Ptr(newCommit.SHA),
1596+
Type: github.Ptr("commit"),
1597+
},
15171598
}
1518-
defer func() { _ = resp.Body.Close() }()
15191599

15201600
r, err := json.Marshal(updatedRef)
15211601
if err != nil {

0 commit comments

Comments
 (0)