A quick tour of making new commits to a repo via the GitHub API using the github3.py library. I'll be making changes to my demodemo repo.
Some useful links:
(Note I am using Python 3.3.)
def public(thing):
"""Print 'public' attributes of thing."""
for x in dir(thing):
if not x.startswith('_'):
print(x)
from github3 import login
username = 'jiffyclub'
token = '---' # not an actual token
The login function will return a GitHub object that remembers my authorization and gives access to the GitHub API.
gh = login(username=username, token=token)
public(gh)
authorization authorize check_authorization create_gist create_issue create_key create_repo delete_key emojis etag feeds follow from_json gist gitignore_template gitignore_templates is_following is_starred is_subscribed issue iter_all_repos iter_all_users iter_authorizations iter_emails iter_events iter_followers iter_following iter_gists iter_issues iter_keys iter_notifications iter_org_issues iter_orgs iter_repo_issues iter_repos iter_starred iter_subscriptions iter_user_issues iter_user_repos iter_user_teams key last_modified login markdown meta octocat organization pubsubhubbub pull_request rate_limit ratelimit_remaining refresh repository search_code search_issues search_repositories search_users set_client_id set_user_agent star subscribe to_json unfollow unstar unsubscribe update_user user zen
The Repository object will be the primary interface for making modifications to a repo (e.g. making new commits).
repo = gh.repository(username, 'demodemo')
public(repo)
add_collaborator archive archive_urlt asset assignees_urlt blob blobs_urlt branch branches_urlt clone_url comments_urlt commit commit_comment commits_urlt compare_commits compare_urlt contents contents_urlt contributors_url create_blob create_comment create_commit create_file create_fork create_hook create_issue create_key create_label create_milestone create_pull create_pull_from_issue create_ref create_release create_status create_tag create_tree created_at default_branch delete delete_file delete_key description download_url edit etag events_url fork fork_count forks from_json full_name git_commit git_commits_urlt git_refs_urlt git_tags_urlt git_url has_downloads has_issues has_wiki homepage hook hooks_url html_url id is_assignee is_collaborator issue issue_comment_urlt issue_events_urlt issues_urlt iter_assignees iter_branches iter_code_frequency iter_comments iter_comments_on_commit iter_commit_activity iter_commits iter_contributor_statistics iter_contributors iter_events iter_forks iter_hooks iter_issue_events iter_issues iter_keys iter_labels iter_languages iter_milestones iter_network_events iter_notifications iter_pulls iter_refs iter_releases iter_stargazers iter_statuses iter_subscribers iter_tags iter_teams key label labels_urlt language languages_url last_modified mark_notifications master_branch merge merges_url milestone milestones_urlt mirror_url name notifications_urlt open_issues open_issues_count owner parent private pull_request pulls_urlt pushed_at ratelimit_remaining readme ref refresh release remove_collaborator set_subscription size source ssh_url stargarzers_url stargazers statuses_urlt subscribers_url subscription subscription_url svn_url tag tags_url teams_url to_json tree trees_urlt update_file update_label updated_at watchers weekly_commit_count
repo.contents('/')
{'README.md': <Content [README.md]>, 'cultural_demodernism.txt': <Content [cultural_demodernism.txt]>, 'new_file.md': <Content [new_file.md]>, 'newfeature.py': <Content [newfeature.py]>, 'postcapitalism.txt': <Content [postcapitalism.txt]>, 'test_file.md': <Content [test_file.md]>}
Get a Contents object representing a single file in the repository.
tf = repo.contents('test_file.md')
public(tf)
content decoded delete encoding etag from_json git_url html_url last_modified links name path ratelimit_remaining refresh sha size submodule_git_url target to_json type update
For most files GitHub sends back the content base64 encoded.
tf.content
'VGVzdGluZyBhIGxpbmsgdG8gdGhlIFtSRUFETUVdKFJFQURNRS5tZCkuCgpN\nYWtpbmcgYSBjaGFuZ2UuCgpUaGlzIGlzIHRoZSBtb3N0IGF3ZXNvbWUgY2hh\nbmdlIGV2ZXIuCg==\n'
I'm using Python 3 so the .decoded
attribute is a bytes object, it's up to me to convert it to a string.
tf.decoded
b'Testing a link to the [README](README.md).\n\nMaking a change.\n\nThis is the most awesome change ever.\n\nA fancy new change via the GitHub API.\n'
content = tf.decoded.decode('utf-8')
content
'Testing a link to the [README](README.md).\n\nMaking a change.\n\nThis is the most awesome change ever.\n'
If I want to modify a single file in a repo the easiest thing to do is get a
Content
object for that file and then use its .update
method to pass the new content.
This will make a new commit on GitHub.
new_content = content + '\nA fancy new change via the GitHub API.\n'
c = tf.update('Trying out the GitHub API via github3.py and Python 3', new_content.encode('utf-8'))
c
<Commit [Matt Davis:4b81be53772de719b4fe08f3dc348cecc01bb45c]>
public(c)
author author_as_User committer committer_as_User etag from_json html_url last_modified message parents ratelimit_remaining refresh sha to_json tree
You can see the commit on GitHub at https://github.com/jiffyclub/demodemo/commit/4b81be53772de719b4fe08f3dc348cecc01bb45c.
The .update
method also updates the branch the Content
object is associated with.
Making a commit that modifies multiple files takes more work. I have to stage each change individually as blobs, make a new tree pointing at the new blobs, and finally create a new commit pointed at the new tree. This is the procedure outlined in the GitHub API docs about Git data.
repo = repo.refresh()
Step one will be to get the current content of some existing files so we can modify them to create new blobs.
files = repo.contents('/')
files
{'README.md': <Content [README.md]>, 'cultural_demodernism.txt': <Content [cultural_demodernism.txt]>, 'new_file.md': <Content [new_file.md]>, 'newfeature.py': <Content [newfeature.py]>, 'postcapitalism.txt': <Content [postcapitalism.txt]>, 'test_file.md': <Content [test_file.md]>}
new_file_content = files['new_file.md'].refresh().decoded.decode('utf-8')
test_file_content = files['test_file.md'].refresh().decoded.decode('utf-8')
new_file_content
'New file!\n\nNew line.\n'
test_file_content
'Testing a link to the [README](README.md).\n\nMaking a change.\n\nThis is the most awesome change ever.\n\nA fancy new change via the GitHub API.\n'
new_file_content = new_file_content + '\nMulti-file commit via the GitHub API!\n'
test_file_content = test_file_content + '\nMulti-file commit via the GitHub API!\n'
new_file_blob = repo.create_blob(new_file_content, encoding='utf-8')
test_file_blob = repo.create_blob(test_file_content, encoding='utf-8')
print(new_file_blob, test_file_blob)
1f3fd85ad07a8231d3ec53a1d85b9dcab791161b bc3db8ab6c49fe54e57d143b282c33d24e2b4731
The new blobs are ready, now to set up a new tree. To create a new tree I'll combine the existing tip tree and the new information from the blobs I just created. To get the sha of the existing tip tree I grab a Branch object and look at its associated commit.
branch = repo.branch(repo.default_branch)
public(branch)
commit etag from_json last_modified links name ratelimit_remaining refresh to_json
tree_sha = branch.commit.commit.tree.sha
Creating a new tree is done with the Repository.create_tree method.
tree_data = [{'path': 'new_file.md', 'mode': '100644', 'type': 'blob', 'sha': new_file_blob},
{'path': 'test_file.md', 'mode': '100644', 'type': 'blob', 'sha': test_file_blob}]
tree = repo.create_tree(tree_data, tree_sha)
tree
<Tree [57c83a4db05753d333ee4b6277d6a1b830220911]>
And finally I can make a new commit with this new tree. The current branch's commit is used as the parent of the new commit.
message = 'Modifying multiple files via the GitHub API and github3.py.'
c = repo.create_commit(message, tree.sha, [branch.commit.sha])
c
<Commit [Matt Davis:eda2a19e23bd52cd50c90b8034763cbc13c3d5fd]>
c.html_url
'https://github.com/jiffyclub/demodemo/commits/eda2a19e23bd52cd50c90b8034763cbc13c3d5fd'
ref = repo.ref('heads/{}'.format(repo.default_branch))
ref.update(c.sha)
True
And now the branch should be pointed to the new commit.
branch.links['html']
'https://github.com/jiffyclub/demodemo/tree/master'
That's a lot of jumping through hoops, but for whatever reason it doesn't seem possible at the moment to make a commit updating multiple files via simpler API calls. I think all that could be wrapped into a function with a definition like:
def multi_commit(files=<a list of paths>, new_content=<list of strings>, branch=<branch name>):
...