@@ -599,12 +599,15 @@ func isTransientStatus(code int) bool {
599599// CommitFiles atomically commits multiple files to the default branch
600600// using the Git Trees/Blobs/Commits API. Returns (false, nil) when
601601// all files already match the current tree (idempotent).
602+ //
603+ // Returns forge.ErrBranchProtected (wrapped) when the ref update fails
604+ // with a 422, which indicates branch protection rules prevent direct pushes.
602605func (c * LiveClient ) CommitFiles (ctx context.Context , owner , repo , message string , files []forge.TreeFile ) (bool , error ) {
603606 if len (files ) == 0 {
604607 return false , nil
605608 }
606609
607- // 1. Get default branch name.
610+ // Get default branch name.
608611 repoResp , err := c .get (ctx , fmt .Sprintf ("/repos/%s/%s" , owner , repo ))
609612 if err != nil {
610613 return false , fmt .Errorf ("get repo: %w" , err )
@@ -616,12 +619,34 @@ func (c *LiveClient) CommitFiles(ctx context.Context, owner, repo, message strin
616619 return false , fmt .Errorf ("decode repo info: %w" , err )
617620 }
618621
619- // 2. Get current commit SHA from the branch ref.
620- // Wrapped in retryOnTransient for freshly-created repos where the
621- // branch ref may not be materialized yet (async auto_init).
622+ committed , err := c .commitFilesTo (ctx , owner , repo , repoInfo .DefaultBranch , message , files )
623+ if err != nil {
624+ var apiErr * APIError
625+ if errors .As (err , & apiErr ) && apiErr .StatusCode == http .StatusUnprocessableEntity {
626+ return false , fmt .Errorf ("%w: %w" , forge .ErrBranchProtected , err )
627+ }
628+ return false , err
629+ }
630+ return committed , nil
631+ }
632+
633+ // CommitFilesToBranch atomically commits multiple files to a specific
634+ // branch. Like CommitFiles, it is idempotent.
635+ func (c * LiveClient ) CommitFilesToBranch (ctx context.Context , owner , repo , branch , message string , files []forge.TreeFile ) (bool , error ) {
636+ if len (files ) == 0 {
637+ return false , nil
638+ }
639+ return c .commitFilesTo (ctx , owner , repo , branch , message , files )
640+ }
641+
642+ // commitFilesTo is the shared implementation for CommitFiles and
643+ // CommitFilesToBranch. It commits files to the specified branch using
644+ // the Git Trees/Blobs/Commits API.
645+ func (c * LiveClient ) commitFilesTo (ctx context.Context , owner , repo , branch , message string , files []forge.TreeFile ) (bool , error ) {
646+ // 1. Get current commit SHA from the branch ref.
622647 var commitSHA string
623648 if err := c .retryOnTransient (ctx , "get branch ref" , func () error {
624- refResp , refErr := c .get (ctx , fmt .Sprintf ("/repos/%s/%s/git/ref/heads/%s" , owner , repo , repoInfo . DefaultBranch ))
649+ refResp , refErr := c .get (ctx , fmt .Sprintf ("/repos/%s/%s/git/ref/heads/%s" , owner , repo , branch ))
625650 if refErr != nil {
626651 return fmt .Errorf ("get branch ref: %w" , refErr )
627652 }
@@ -639,7 +664,7 @@ func (c *LiveClient) CommitFiles(ctx context.Context, owner, repo, message strin
639664 return false , err
640665 }
641666
642- // 3 . Get the current commit to find its tree SHA.
667+ // 2 . Get the current commit to find its tree SHA.
643668 cResp , err := c .get (ctx , fmt .Sprintf ("/repos/%s/%s/git/commits/%s" , owner , repo , commitSHA ))
644669 if err != nil {
645670 return false , fmt .Errorf ("get commit: %w" , err )
@@ -654,7 +679,7 @@ func (c *LiveClient) CommitFiles(ctx context.Context, owner, repo, message strin
654679 }
655680 baseTreeSHA := commitObj .Tree .SHA
656681
657- // 4 . Get the full recursive tree to compare existing blobs.
682+ // 3 . Get the full recursive tree to compare existing blobs.
658683 treeResp , err := c .get (ctx , fmt .Sprintf ("/repos/%s/%s/git/trees/%s?recursive=1" , owner , repo , baseTreeSHA ))
659684 if err != nil {
660685 return false , fmt .Errorf ("get tree: %w" , err )
@@ -683,7 +708,7 @@ func (c *LiveClient) CommitFiles(ctx context.Context, owner, repo, message strin
683708 existing [entry .Path ] = blobInfo {sha : entry .SHA , mode : entry .Mode }
684709 }
685710
686- // 5 . Compute expected blob SHAs and filter to changed files.
711+ // 4 . Compute expected blob SHAs and filter to changed files.
687712 var changedEntries []map [string ]string
688713 for _ , f := range files {
689714 expectedSHA := blobSHA (f .Content )
@@ -702,7 +727,7 @@ func (c *LiveClient) CommitFiles(ctx context.Context, owner, repo, message strin
702727 return false , nil
703728 }
704729
705- // 6 . Create new tree with base_tree + changed entries.
730+ // 5 . Create new tree with base_tree + changed entries.
706731 treePayload := map [string ]any {
707732 "base_tree" : baseTreeSHA ,
708733 "tree" : changedEntries ,
@@ -718,7 +743,7 @@ func (c *LiveClient) CommitFiles(ctx context.Context, owner, repo, message strin
718743 return false , fmt .Errorf ("decode new tree: %w" , err )
719744 }
720745
721- // 7 . Create commit with new tree and old commit as parent.
746+ // 6 . Create commit with new tree and old commit as parent.
722747 commitPayload := map [string ]any {
723748 "message" : message ,
724749 "tree" : newTree .SHA ,
@@ -735,11 +760,11 @@ func (c *LiveClient) CommitFiles(ctx context.Context, owner, repo, message strin
735760 return false , fmt .Errorf ("decode new commit: %w" , err )
736761 }
737762
738- // 8 . Update branch ref to point to new commit.
763+ // 7 . Update branch ref to point to new commit.
739764 refPayload := map [string ]string {
740765 "sha" : newCommit .SHA ,
741766 }
742- refUpdateResp , err := c .patch (ctx , fmt .Sprintf ("/repos/%s/%s/git/refs/heads/%s" , owner , repo , repoInfo . DefaultBranch ), refPayload )
767+ refUpdateResp , err := c .patch (ctx , fmt .Sprintf ("/repos/%s/%s/git/refs/heads/%s" , owner , repo , branch ), refPayload )
743768 if err != nil {
744769 return false , fmt .Errorf ("update ref: %w" , err )
745770 }
0 commit comments