@@ -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+
27118func 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