@@ -2,7 +2,10 @@ package git
22
33import (
44 "bytes"
5+ "context"
6+ "encoding/base64"
57 "fmt"
8+ "io"
69 "os"
710 "os/exec"
811 "path/filepath"
@@ -11,7 +14,9 @@ import (
1114
1215 "github.com/go-git/go-git/v5"
1316 "github.com/go-git/go-git/v5/plumbing"
17+ "github.com/shurcooL/githubv4"
1418 "github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/utils"
19+ "golang.org/x/oauth2"
1520)
1621
1722// FindChangedFiles executes a git diff against a specified base reference and pipes the output through a user-defined grep command or sequence.
@@ -261,3 +266,147 @@ func GetOwnerRepoDefaultBranchFromLocalRepo(repoPath string) (owner, repoName, d
261266
262267 return owner , repoName , defaultBranch , nil
263268}
269+
270+ // MakeSignedCommit adds all changes to a repo and creates a signed commit for GitHub
271+ func MakeSignedCommit (repoPath , commitMessage , branch , githubToken string ) (string , error ) {
272+ tok := oauth2 .StaticTokenSource (& oauth2.Token {AccessToken : githubToken })
273+ token := oauth2 .NewClient (context .Background (), tok )
274+ graphqlClient := githubv4 .NewClient (token )
275+
276+ repo , err := git .PlainOpen (repoPath )
277+ if err != nil {
278+ return "" , err
279+ }
280+
281+ // Code mostly stolen from https://github.com/planetscale/ghcommit/tree/main
282+
283+ // process added / modified files:
284+ worktree , err := repo .Worktree ()
285+ if err != nil {
286+ return "" , err
287+ }
288+
289+ // Get the status of all files in the worktree
290+ status , err := worktree .Status ()
291+ if err != nil {
292+ return "" , err
293+ }
294+
295+ additions := []githubv4.FileAddition {}
296+ deletions := []githubv4.FileDeletion {}
297+
298+ // Process each file based on its status
299+ for filePath , fileStatus := range status {
300+ switch fileStatus .Staging {
301+ case git .Added , git .Modified :
302+ // File is added or modified - add to additions
303+ enc , err := base64EncodeFile (filepath .Join (repoPath , filePath ))
304+ if err != nil {
305+ return "" , err
306+ }
307+ additions = append (additions , githubv4.FileAddition {
308+ Path : githubv4 .String (filePath ),
309+ Contents : githubv4 .Base64String (enc ),
310+ })
311+ case git .Deleted :
312+ // File is deleted - add to deletions
313+ deletions = append (deletions , githubv4.FileDeletion {
314+ Path : githubv4 .String (filePath ),
315+ })
316+ }
317+
318+ // Also check worktree status (unstaged changes)
319+ switch fileStatus .Worktree {
320+ case git .Modified :
321+ // Only add if not already processed from staging
322+ if fileStatus .Staging != git .Added && fileStatus .Staging != git .Modified {
323+ enc , err := base64EncodeFile (filepath .Join (repoPath , filePath ))
324+ if err != nil {
325+ return "" , err
326+ }
327+ additions = append (additions , githubv4.FileAddition {
328+ Path : githubv4 .String (filePath ),
329+ Contents : githubv4 .Base64String (enc ),
330+ })
331+ }
332+ case git .Deleted :
333+ // Only add if not already processed from staging
334+ if fileStatus .Staging != git .Deleted {
335+ deletions = append (deletions , githubv4.FileDeletion {
336+ Path : githubv4 .String (filePath ),
337+ })
338+ }
339+ }
340+ }
341+
342+ var m struct {
343+ CreateCommitOnBranch struct {
344+ Commit struct {
345+ URL string `graphql:"url"`
346+ OID string `graphql:"oid"`
347+ Additions int `graphql:"additions"`
348+ Deletions int `graphql:"deletions"`
349+ }
350+ } `graphql:"createCommitOnBranch(input:$input)"`
351+ }
352+
353+ splitMsg := strings .SplitN (commitMessage , "\n " , 2 )
354+ headline := splitMsg [0 ]
355+ body := ""
356+ if len (splitMsg ) > 1 {
357+ body = splitMsg [1 ]
358+ }
359+
360+ owner , repoName , _ , err := GetOwnerRepoDefaultBranchFromLocalRepo (repoPath )
361+ if err != nil {
362+ return "" , err
363+ }
364+
365+ // Get HEAD reference to get the current commit hash
366+ headRef , err := repo .Head ()
367+ if err != nil {
368+ return "" , fmt .Errorf ("failed to get HEAD reference: %w" , err )
369+ }
370+ expectedHeadOid := headRef .Hash ().String ()
371+ // create the $input struct for the graphQL createCommitOnBranch mutation request:
372+ input := githubv4.CreateCommitOnBranchInput {
373+ Branch : githubv4.CommittableBranch {
374+ RepositoryNameWithOwner : githubv4 .NewString (githubv4 .String (fmt .Sprintf ("%s/%s" , owner , repoName ))),
375+ BranchName : githubv4 .NewString (githubv4 .String (branch )),
376+ },
377+ Message : githubv4.CommitMessage {
378+ Headline : githubv4 .String (headline ),
379+ Body : githubv4 .NewString (githubv4 .String (body )),
380+ },
381+ FileChanges : & githubv4.FileChanges {
382+ Additions : & additions ,
383+ Deletions : & deletions ,
384+ },
385+ ExpectedHeadOid : githubv4 .GitObjectID (expectedHeadOid ),
386+ }
387+
388+ if err := graphqlClient .Mutate (context .Background (), & m , input , nil ); err != nil {
389+ return "" , err
390+ }
391+
392+ return m .CreateCommitOnBranch .Commit .OID , nil
393+ }
394+
395+ func base64EncodeFile (path string ) (string , error ) {
396+ in , err := os .Open (path )
397+ if err != nil {
398+ return "" , err
399+ }
400+ defer in .Close () // nolint: errcheck
401+
402+ buf := bytes.Buffer {}
403+ encoder := base64 .NewEncoder (base64 .StdEncoding , & buf )
404+
405+ if _ , err := io .Copy (encoder , in ); err != nil {
406+ return "" , err
407+ }
408+ if err := encoder .Close (); err != nil {
409+ return "" , err
410+ }
411+ return buf .String (), nil
412+ }
0 commit comments