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.
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:
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())
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With