Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Edit multiple files in single commit with GitHub API

I'm using the GitHub API in a C# webapp (with Blazor). I want to be able to create a single commit to add, delete, and edit multiple files in a folder in a repository. I know I can send a PUT request to the URL https://api.github.com/repos/[username]/[repository]/contents/[file] with these contents to create a file (and I can also edit a file by adding an SHA hash):

{
    "message": "[Commit message]",
    "content": "[Content encoded in base64]",
    "committer": {
        "name": "[Committer name]",
        "email": "[Committer email]"
    }
}

But this creates one commit for every file change. Is there any way that I can do multiple operations in a single commit (either using the GitHub API or something else)? I would use something like libgit2sharp but I don't want to be cloning the repository to a folder on the filesystem.

like image 420
Merlin04 Avatar asked Sep 07 '26 21:09

Merlin04


2 Answers

Is there any way that I can do multiple operations in a single commit (either using the GitHub API or something else)?

There is the underlying Git Data API that can be used to build up a commit from scratch:

  • files are uploaded as blobs using the API
  • trees are used to indicate what the repository state should be (update paths to point to the new blobs)
  • then create a new commit using the new root tree and additional metadata
  • if you can, then update the reference (i.e. the branch) to point to this new commit
like image 191
Brendan Forster Avatar answered Sep 10 '26 13:09

Brendan Forster


The following code uploads a.txt, b.txt and c.txt to the repository root directory.

To make the following code work, replace 'a.txt', 'b.txt', 'c.txt' with your files, owner/repo with your username/repository and github_pat_********* with your GitHub token.

The mechanism is explained in this answer.

import asyncio
from base64 import b64encode
from pathlib import Path

import aiohttp

async def upload(filenames: list[str], repo: str, token: str) -> None:
    async def create_blob(filename: str) -> str:
        async with session.post('blobs', json={'content': b64encode(Path(filename).read_bytes()).decode(),
                                                    'encoding': 'base64'}) as response:
            return (await response.json())['sha']

    async def get_parent_commit() -> str:
        async with session.get(f'refs/heads/{default_branch}') as response:
            return (await response.json())['object']['sha']

    headers = {'Authorization': f'token {token}', 'Accept': 'application/vnd.github+json'}
    async with aiohttp.ClientSession(f'https://api.github.com/repos/{repo}/git/', headers=headers) as session:
        async with asyncio.TaskGroup() as tg:
            # create blobs and get blob_shas
            blob_sha_tasks = [tg.create_task(create_blob(filename)) for filename in filenames]
            # get default_branch
            async with session.get(f'https://api.github.com/repos/{repo}') as response:
                default_branch = (await response.json())['default_branch']
            # get parent_commit
            parent_commit_task = tg.create_task(get_parent_commit())
            # get base_tree
            async with session.get(f'trees/{default_branch}') as response:
                tree = {'base_tree': (await response.json())['sha'],
                        'tree': [{'path': filename, 'mode': '100644', 'type': 'blob', 'sha': await task}
                                 for filename, task in zip(filenames, blob_sha_tasks)]}
            # create tree
            async with session.post('trees', json=tree) as response:
                commit = {'tree': (await response.json())['sha'], 'parents': [await parent_commit_task],
                          'message': 'example message', 'author': {'name': 'GitHub Actions',
                                                                   'email': 'github-actions[bot]@users.noreply.github.com'}}
        # create commit
        async with session.post('commits', json=commit) as response:
            commit_sha = (await response.json())['sha']
        # set branch to the commit
        async with session.patch(f'refs/heads/{default_branch}', json={'sha': commit_sha}) as response:
            assert response.status == 200, 'upload failed'

async def main() -> None:
    await upload(['a.txt', 'b.txt', 'c.txt'], 'owner/repo', 'github_pat_**********')

asyncio.run(main())
like image 44
pegasus Avatar answered Sep 10 '26 12:09

pegasus



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!