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.)

In [27]:
def public(thing):
    """Print 'public' attributes of thing."""
    for x in dir(thing):
        if not x.startswith('_'):
            print(x)
In [1]:
from github3 import login
In [4]:
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.

In [5]:
gh = login(username=username, token=token)
In [28]:
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).

In [ ]:
repo = gh.repository(username, 'demodemo')
In [29]:
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
In [12]:
repo.contents('/')
Out[12]:
{'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]>}

Modify a Single File

Get a Contents object representing a single file in the repository.

In [33]:
tf = repo.contents('test_file.md')
In [30]:
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.

In [15]:
tf.content
Out[15]:
'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.

In [34]:
tf.decoded
Out[34]:
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'
In [17]:
content = tf.decoded.decode('utf-8')
In [18]:
content
Out[18]:
'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.

In [20]:
new_content = content + '\nA fancy new change via the GitHub API.\n'
In [22]:
c = tf.update('Trying out the GitHub API via github3.py and Python 3', new_content.encode('utf-8'))
In [23]:
c
Out[23]:
<Commit [Matt Davis:4b81be53772de719b4fe08f3dc348cecc01bb45c]>
In [31]:
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.

Modify Multiple Files

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.

In [46]:
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.

In [47]:
files = repo.contents('/')
files
Out[47]:
{'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]>}
In [59]:
new_file_content = files['new_file.md'].refresh().decoded.decode('utf-8')
test_file_content = files['test_file.md'].refresh().decoded.decode('utf-8')
In [60]:
new_file_content
Out[60]:
'New file!\n\nNew line.\n'
In [61]:
test_file_content
Out[61]:
'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'
In [62]:
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'
In [65]:
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.

In [68]:
branch = repo.branch(repo.default_branch)
In [69]:
public(branch)
commit
etag
from_json
last_modified
links
name
ratelimit_remaining
refresh
to_json
In [82]:
tree_sha = branch.commit.commit.tree.sha

Creating a new tree is done with the Repository.create_tree method.

In [87]:
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
Out[87]:
<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.

In [90]:
message = 'Modifying multiple files via the GitHub API and github3.py.'
c = repo.create_commit(message, tree.sha, [branch.commit.sha])
c
Out[90]:
<Commit [Matt Davis:eda2a19e23bd52cd50c90b8034763cbc13c3d5fd]>
In [91]:
c.html_url
Out[91]:
'https://github.com/jiffyclub/demodemo/commits/eda2a19e23bd52cd50c90b8034763cbc13c3d5fd'

At this point I've made the commit but the 'master' branch reference is still pointed at the previous commit. I'll need to make another API call to update where the branch is pointed.

In [92]:
ref = repo.ref('heads/{}'.format(repo.default_branch))
In [93]:
ref.update(c.sha)
Out[93]:
True

And now the branch should be pointed to the new commit.

In [95]:
branch.links['html']
Out[95]:
'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>):
    ...
In [ ]: