7
7
"fmt"
8
8
"net/url"
9
9
"os"
10
+ "path/filepath"
10
11
"strings"
11
12
"time"
12
13
@@ -16,6 +17,9 @@ import (
16
17
"github.com/go-git/go-git/v5/plumbing"
17
18
"github.com/go-git/go-git/v5/plumbing/object"
18
19
"github.com/go-git/go-git/v5/plumbing/transport/http"
20
+ "github.com/go-git/go-git/v5/utils/merkletrie"
21
+ "github.com/google/go-github/v57/github"
22
+ "github.com/shurcooL/githubv4"
19
23
"github.com/sirupsen/logrus"
20
24
)
21
25
@@ -63,6 +67,81 @@ func cloneGitRepository(ctx context.Context, repo Repository, localPath string,
63
67
return gitRepo , nil
64
68
}
65
69
70
+ type createBranchOptions struct {
71
+ GitHubOpts GitHubOptions
72
+ Repository Repository
73
+ BranchName string
74
+ CommitSHA string
75
+ }
76
+
77
+ func createBranchWithAPI (ctx context.Context , opts createBranchOptions ) error {
78
+ client , _ , err := githubClient (ctx , opts .GitHubOpts )
79
+ if err != nil {
80
+ return fmt .Errorf ("failed to create github client: %w" , err )
81
+ }
82
+
83
+ repository , _ , err := client .Repositories .Get (ctx , opts .Repository .Owner , opts .Repository .Name )
84
+ if err != nil {
85
+ return fmt .Errorf ("failed to fetch repository: %w" , err )
86
+ }
87
+
88
+ gqlClient , err := githubGraphqlClient (ctx , opts .GitHubOpts )
89
+ if err != nil {
90
+ return fmt .Errorf ("failed to create github GraphQL client: %w" , err )
91
+ }
92
+
93
+ inputs := githubv4.CreateRefInput {
94
+ RepositoryID : githubv4 .ID (repository .NodeID ),
95
+ Name : githubv4 .String (fmt .Sprintf ("refs/heads/%s" , opts .BranchName )),
96
+ Oid : githubv4 .GitObjectID (opts .CommitSHA ),
97
+ }
98
+
99
+ var mutation struct {
100
+ CreateRefInput struct {
101
+ ClientMutationID string
102
+ } `graphql:"createRef(input: $input)"`
103
+ }
104
+
105
+ err = gqlClient .Mutate (ctx , & mutation , inputs , nil )
106
+ if err != nil {
107
+ return fmt .Errorf ("failed to create branch: %w" , err )
108
+ }
109
+ return nil
110
+ }
111
+
112
+ type resetBranchOptions struct {
113
+ GitHubOpts GitHubOptions
114
+ Repository Repository
115
+ BranchName string
116
+ CommitSHA string
117
+ }
118
+
119
+ func resetBranchWithAPI (ctx context.Context , opts resetBranchOptions ) error {
120
+ client , _ , err := githubClient (ctx , opts .GitHubOpts )
121
+ if err != nil {
122
+ return fmt .Errorf ("failed to create github client: %w" , err )
123
+ }
124
+
125
+ branchRef := fmt .Sprintf ("refs/heads/%s" , opts .BranchName )
126
+
127
+ _ , _ , err = client .Git .UpdateRef (
128
+ ctx ,
129
+ opts .Repository .Owner ,
130
+ opts .Repository .Name ,
131
+ & github.Reference {
132
+ Ref : & branchRef ,
133
+ Object : & github.GitObject {
134
+ SHA : & opts .CommitSHA ,
135
+ },
136
+ },
137
+ true ,
138
+ )
139
+ if err != nil {
140
+ return fmt .Errorf ("failed to update branch ref: %w" , err )
141
+ }
142
+ return nil
143
+ }
144
+
66
145
type switchBranchOptions struct {
67
146
Repository Repository
68
147
BranchName string
@@ -198,14 +277,146 @@ func parseSigningKey(signingKeyPath, signingKeyPassphrase string) (*openpgp.Enti
198
277
return signingKey , nil
199
278
}
200
279
201
- type pushOptions struct {
202
- GitHubOpts GitHubOptions
203
- Repository Repository
204
- BranchName string
205
- ResetFromBase bool
280
+ func getLatestCommit (_ context.Context , gitRepo * git.Repository ) (* object.Commit , error ) {
281
+ headCommitRef , err := gitRepo .Head ()
282
+ if err != nil {
283
+ return nil , fmt .Errorf ("failed to fetch head: %w" , err )
284
+ }
285
+
286
+ latestCommit , err := gitRepo .CommitObject (headCommitRef .Hash ())
287
+ if err != nil {
288
+ return nil , fmt .Errorf ("failed to fetch commit: %w" , err )
289
+ }
290
+ return latestCommit , nil
206
291
}
207
292
208
- func pushChanges (ctx context.Context , gitRepo * git.Repository , opts pushOptions ) error {
293
+ func compareCommits (base , head * object.Commit ) (* CommitFileChanges , error ) {
294
+ baseTree , err := base .Tree ()
295
+ if err != nil {
296
+ return nil , fmt .Errorf ("failed to get base commit tree: %w" , err )
297
+ }
298
+
299
+ headTree , err := head .Tree ()
300
+ if err != nil {
301
+ return nil , fmt .Errorf ("failed to get head commit tree: %w" , err )
302
+ }
303
+
304
+ changes , err := baseTree .Diff (headTree )
305
+ if err != nil {
306
+ return nil , fmt .Errorf ("failed to compare commit trees: %w" , err )
307
+ }
308
+
309
+ commitFileChanges := CommitFileChanges {}
310
+ for _ , change := range changes {
311
+ action , err := change .Action ()
312
+ if err != nil {
313
+ return nil , fmt .Errorf ("failed to get commit change action: %w" , err )
314
+ }
315
+
316
+ if action == merkletrie .Delete {
317
+ commitFileChanges .Deleted = append (commitFileChanges .Deleted , change .From .Name )
318
+ } else {
319
+ commitFileChanges .Upserted = append (commitFileChanges .Upserted , change .To .Name )
320
+ }
321
+ }
322
+ return & commitFileChanges , nil
323
+ }
324
+
325
+ func pushChangesWithAPI (ctx context.Context , gitRepo * git.Repository , opts pushOptions ) error {
326
+ commit , err := getLatestCommit (ctx , gitRepo )
327
+ if err != nil {
328
+ return fmt .Errorf ("failed to fetch latest commit: %w" , err )
329
+ }
330
+
331
+ parentCommit , err := commit .Parent (0 )
332
+ if err != nil {
333
+ return fmt .Errorf ("failed to fetch parent of latest commit: %w" , err )
334
+ }
335
+
336
+ parentCommitSHA := parentCommit .Hash .String ()
337
+
338
+ if opts .CreateBranch {
339
+ err = createBranchWithAPI (ctx , createBranchOptions {
340
+ GitHubOpts : opts .GitHubOpts ,
341
+ Repository : opts .Repository ,
342
+ BranchName : opts .BranchName ,
343
+ CommitSHA : parentCommitSHA ,
344
+ })
345
+ if err != nil {
346
+ return fmt .Errorf ("failed to create branch: %w" , err )
347
+ }
348
+ } else if opts .ResetFromBase {
349
+ err = resetBranchWithAPI (ctx , resetBranchOptions {
350
+ GitHubOpts : opts .GitHubOpts ,
351
+ Repository : opts .Repository ,
352
+ BranchName : opts .BranchName ,
353
+ CommitSHA : parentCommitSHA ,
354
+ })
355
+ if err != nil {
356
+ return fmt .Errorf ("failed to reset branch: %w" , err )
357
+ }
358
+ }
359
+
360
+ changes , err := compareCommits (parentCommit , commit )
361
+ if err != nil {
362
+ return fmt .Errorf ("failed to compare commits: %w" , err )
363
+ }
364
+
365
+ deletions := make ([]githubv4.FileDeletion , 0 , len (changes .Deleted ))
366
+ for _ , path := range changes .Deleted {
367
+ deletions = append (deletions , githubv4.FileDeletion {
368
+ Path : githubv4 .String (path ),
369
+ })
370
+ }
371
+
372
+ additions := make ([]githubv4.FileAddition , 0 , len (changes .Upserted ))
373
+ repoDirPath := filepath .Join (opts .GitCloneDir , opts .Repository .Owner , opts .Repository .Name )
374
+ for _ , path := range changes .Upserted {
375
+ base64FileContent , err := base64EncodeFile (filepath .Join (repoDirPath , path ))
376
+ if err != nil {
377
+ return fmt .Errorf ("failed to encode file to base64: %w" , err )
378
+ }
379
+ additions = append (additions , githubv4.FileAddition {
380
+ Path : githubv4 .String (path ),
381
+ Contents : githubv4 .Base64String (base64FileContent ),
382
+ })
383
+ }
384
+
385
+ inputs := githubv4.CreateCommitOnBranchInput {
386
+ Branch : githubv4.CommittableBranch {
387
+ RepositoryNameWithOwner : githubv4 .NewString (githubv4 .String (opts .Repository .FullName ())),
388
+ BranchName : githubv4 .NewString (githubv4 .String (opts .BranchName )),
389
+ },
390
+ Message : githubv4.CommitMessage {
391
+ Headline : githubv4 .String (opts .CommitMessage .Headline ),
392
+ Body : githubv4 .NewString (githubv4 .String (opts .CommitMessage .Body )),
393
+ },
394
+ FileChanges : & githubv4.FileChanges {
395
+ Additions : & additions ,
396
+ Deletions : & deletions ,
397
+ },
398
+ ExpectedHeadOid : githubv4 .GitObjectID (parentCommitSHA ),
399
+ }
400
+
401
+ var mutation struct {
402
+ CreateCommitOnBranchInput struct {
403
+ ClientMutationID string
404
+ } `graphql:"createCommitOnBranch(input: $input)"`
405
+ }
406
+
407
+ gqlClient , err := githubGraphqlClient (ctx , opts .GitHubOpts )
408
+ if err != nil {
409
+ return fmt .Errorf ("failed to create github GraphQL client: %w" , err )
410
+ }
411
+
412
+ err = gqlClient .Mutate (ctx , & mutation , inputs , nil )
413
+ if err != nil {
414
+ return fmt .Errorf ("failed to push branch %s to %s: %w" , opts .BranchName , opts .Repository .FullName (), err )
415
+ }
416
+ return nil
417
+ }
418
+
419
+ func pushChangesWithGit (ctx context.Context , gitRepo * git.Repository , opts pushOptions ) error {
209
420
refSpec := fmt .Sprintf ("refs/heads/%[1]s:refs/heads/%[1]s" , opts .BranchName )
210
421
if opts .ResetFromBase {
211
422
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
@@ -242,3 +453,24 @@ func pushChanges(ctx context.Context, gitRepo *git.Repository, opts pushOptions)
242
453
}).Debug ("Git changes pushed" )
243
454
return nil
244
455
}
456
+
457
+ type pushOptions struct {
458
+ GitHubOpts GitHubOptions
459
+ GitCloneDir string
460
+ Repository Repository
461
+ BranchName string
462
+ CreateBranch bool
463
+ ResetFromBase bool
464
+ CommitMessage CommitMessage
465
+ }
466
+
467
+ func pushChanges (ctx context.Context , gitRepo * git.Repository , opts pushOptions ) error {
468
+ switch opts .GitHubOpts .AuthMethod {
469
+ case "token" :
470
+ return pushChangesWithGit (ctx , gitRepo , opts )
471
+ case "app" :
472
+ return pushChangesWithAPI (ctx , gitRepo , opts )
473
+ default :
474
+ return fmt .Errorf ("GitHub auth method unrecognized (allowed values: app, token): %s" , opts .GitHubOpts .AuthMethod )
475
+ }
476
+ }
0 commit comments