Skip to content

Instantly share code, notes, and snippets.

@jiffyclub
Created February 26, 2014 18:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jiffyclub/9235955 to your computer and use it in GitHub Desktop.
Save jiffyclub/9235955 to your computer and use it in GitHub Desktop.
Examples of using github3.py to make git commits via the GitHub API.
Display the source blob
Display the rendered blob
Raw
{
"metadata": {
"name": ""
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A quick tour of making new commits to a repo via the GitHub API using\n",
"the github3.py library.\n",
"I'll be making changes to my [demodemo](https://github.com/jiffyclub/demodemo) repo.\n",
"\n",
"Some useful links:\n",
"\n",
"- [GitHub API docs](http://developer.github.com/)\n",
"- [github3.py docs](http://github3py.readthedocs.org/)\n",
"\n",
"(Note I am using Python 3.3.)"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def public(thing):\n",
" \"\"\"Print 'public' attributes of thing.\"\"\"\n",
" for x in dir(thing):\n",
" if not x.startswith('_'):\n",
" print(x)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 27
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"from github3 import login"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 1
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"username = 'jiffyclub'\n",
"token = '---' # not an actual token"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 4
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The [login](http://github3py.readthedocs.org/en/latest/api.html#github3.login) function will return a\n",
"[GitHub object](http://github3py.readthedocs.org/en/latest/github.html#github3.github.GitHub) that remembers\n",
"my authorization and gives access to the GitHub API."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"gh = login(username=username, token=token)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 5
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"public(gh)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"authorization\n",
"authorize\n",
"check_authorization\n",
"create_gist\n",
"create_issue\n",
"create_key\n",
"create_repo\n",
"delete_key\n",
"emojis\n",
"etag\n",
"feeds\n",
"follow\n",
"from_json\n",
"gist\n",
"gitignore_template\n",
"gitignore_templates\n",
"is_following\n",
"is_starred\n",
"is_subscribed\n",
"issue\n",
"iter_all_repos\n",
"iter_all_users\n",
"iter_authorizations\n",
"iter_emails\n",
"iter_events\n",
"iter_followers\n",
"iter_following\n",
"iter_gists\n",
"iter_issues\n",
"iter_keys\n",
"iter_notifications\n",
"iter_org_issues\n",
"iter_orgs\n",
"iter_repo_issues\n",
"iter_repos\n",
"iter_starred\n",
"iter_subscriptions\n",
"iter_user_issues\n",
"iter_user_repos\n",
"iter_user_teams\n",
"key\n",
"last_modified\n",
"login\n",
"markdown\n",
"meta\n",
"octocat\n",
"organization\n",
"pubsubhubbub\n",
"pull_request\n",
"rate_limit\n",
"ratelimit_remaining\n",
"refresh\n",
"repository\n",
"search_code\n",
"search_issues\n",
"search_repositories\n",
"search_users\n",
"set_client_id\n",
"set_user_agent\n",
"star\n",
"subscribe\n",
"to_json\n",
"unfollow\n",
"unstar\n",
"unsubscribe\n",
"update_user\n",
"user\n",
"zen\n"
]
}
],
"prompt_number": 28
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The [Repository](http://github3py.readthedocs.org/en/latest/repos.html#repository-objects)\n",
"object will be the primary interface for making modifications to a repo\n",
"(e.g. making new commits)."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"repo = gh.repository(username, 'demodemo')"
],
"language": "python",
"metadata": {},
"outputs": []
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"public(repo)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"add_collaborator\n",
"archive\n",
"archive_urlt\n",
"asset\n",
"assignees_urlt\n",
"blob\n",
"blobs_urlt\n",
"branch\n",
"branches_urlt\n",
"clone_url\n",
"comments_urlt\n",
"commit\n",
"commit_comment\n",
"commits_urlt\n",
"compare_commits\n",
"compare_urlt\n",
"contents\n",
"contents_urlt\n",
"contributors_url\n",
"create_blob\n",
"create_comment\n",
"create_commit\n",
"create_file\n",
"create_fork\n",
"create_hook\n",
"create_issue\n",
"create_key\n",
"create_label\n",
"create_milestone\n",
"create_pull\n",
"create_pull_from_issue\n",
"create_ref\n",
"create_release\n",
"create_status\n",
"create_tag\n",
"create_tree\n",
"created_at\n",
"default_branch\n",
"delete\n",
"delete_file\n",
"delete_key\n",
"description\n",
"download_url\n",
"edit\n",
"etag\n",
"events_url\n",
"fork\n",
"fork_count\n",
"forks\n",
"from_json\n",
"full_name\n",
"git_commit\n",
"git_commits_urlt\n",
"git_refs_urlt\n",
"git_tags_urlt\n",
"git_url\n",
"has_downloads\n",
"has_issues\n",
"has_wiki\n",
"homepage\n",
"hook\n",
"hooks_url\n",
"html_url\n",
"id\n",
"is_assignee\n",
"is_collaborator\n",
"issue\n",
"issue_comment_urlt\n",
"issue_events_urlt\n",
"issues_urlt\n",
"iter_assignees\n",
"iter_branches\n",
"iter_code_frequency\n",
"iter_comments\n",
"iter_comments_on_commit\n",
"iter_commit_activity\n",
"iter_commits\n",
"iter_contributor_statistics\n",
"iter_contributors\n",
"iter_events\n",
"iter_forks\n",
"iter_hooks\n",
"iter_issue_events\n",
"iter_issues\n",
"iter_keys\n",
"iter_labels\n",
"iter_languages\n",
"iter_milestones\n",
"iter_network_events\n",
"iter_notifications\n",
"iter_pulls\n",
"iter_refs\n",
"iter_releases\n",
"iter_stargazers\n",
"iter_statuses\n",
"iter_subscribers\n",
"iter_tags\n",
"iter_teams\n",
"key\n",
"label\n",
"labels_urlt\n",
"language\n",
"languages_url\n",
"last_modified\n",
"mark_notifications\n",
"master_branch\n",
"merge\n",
"merges_url\n",
"milestone\n",
"milestones_urlt\n",
"mirror_url\n",
"name\n",
"notifications_urlt\n",
"open_issues\n",
"open_issues_count\n",
"owner\n",
"parent\n",
"private\n",
"pull_request\n",
"pulls_urlt\n",
"pushed_at\n",
"ratelimit_remaining\n",
"readme\n",
"ref\n",
"refresh\n",
"release\n",
"remove_collaborator\n",
"set_subscription\n",
"size\n",
"source\n",
"ssh_url\n",
"stargarzers_url\n",
"stargazers\n",
"statuses_urlt\n",
"subscribers_url\n",
"subscription\n",
"subscription_url\n",
"svn_url\n",
"tag\n",
"tags_url\n",
"teams_url\n",
"to_json\n",
"tree\n",
"trees_urlt\n",
"update_file\n",
"update_label\n",
"updated_at\n",
"watchers\n",
"weekly_commit_count\n"
]
}
],
"prompt_number": 29
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"repo.contents('/')"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 12,
"text": [
"{'README.md': <Content [README.md]>,\n",
" 'cultural_demodernism.txt': <Content [cultural_demodernism.txt]>,\n",
" 'new_file.md': <Content [new_file.md]>,\n",
" 'newfeature.py': <Content [newfeature.py]>,\n",
" 'postcapitalism.txt': <Content [postcapitalism.txt]>,\n",
" 'test_file.md': <Content [test_file.md]>}"
]
}
],
"prompt_number": 12
},
{
"cell_type": "heading",
"level": 1,
"metadata": {},
"source": [
"Modify a Single File"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Get a [Contents](http://github3py.readthedocs.org/en/latest/repos.html#github3.repos.contents.Contents)\n",
"object representing a single file in the repository."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"tf = repo.contents('test_file.md')"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 33
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"public(tf)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"content\n",
"decoded\n",
"delete\n",
"encoding\n",
"etag\n",
"from_json\n",
"git_url\n",
"html_url\n",
"last_modified\n",
"links\n",
"name\n",
"path\n",
"ratelimit_remaining\n",
"refresh\n",
"sha\n",
"size\n",
"submodule_git_url\n",
"target\n",
"to_json\n",
"type\n",
"update\n"
]
}
],
"prompt_number": 30
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For most files GitHub sends back the content base64 encoded."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"tf.content"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 15,
"text": [
"'VGVzdGluZyBhIGxpbmsgdG8gdGhlIFtSRUFETUVdKFJFQURNRS5tZCkuCgpN\\nYWtpbmcgYSBjaGFuZ2UuCgpUaGlzIGlzIHRoZSBtb3N0IGF3ZXNvbWUgY2hh\\nbmdlIGV2ZXIuCg==\\n'"
]
}
],
"prompt_number": 15
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I'm using Python 3 so the `.decoded` attribute is a bytes object, it's up to me to convert it to a string."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"tf.decoded"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 34,
"text": [
"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'"
]
}
],
"prompt_number": 34
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"content = tf.decoded.decode('utf-8')"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 17
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"content"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 18,
"text": [
"'Testing a link to the [README](README.md).\\n\\nMaking a change.\\n\\nThis is the most awesome change ever.\\n'"
]
}
],
"prompt_number": 18
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If I want to modify a single file in a repo the easiest thing to do is get a\n",
"`Content` object for that file and then use its `.update` method to pass the new content.\n",
"This will make a new commit on GitHub."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"new_content = content + '\\nA fancy new change via the GitHub API.\\n'"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 20
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"c = tf.update('Trying out the GitHub API via github3.py and Python 3', new_content.encode('utf-8'))"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 22
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"c"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 23,
"text": [
"<Commit [Matt Davis:4b81be53772de719b4fe08f3dc348cecc01bb45c]>"
]
}
],
"prompt_number": 23
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"public(c)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"author\n",
"author_as_User\n",
"committer\n",
"committer_as_User\n",
"etag\n",
"from_json\n",
"html_url\n",
"last_modified\n",
"message\n",
"parents\n",
"ratelimit_remaining\n",
"refresh\n",
"sha\n",
"to_json\n",
"tree\n"
]
}
],
"prompt_number": 31
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can see the commit on GitHub at https://github.com/jiffyclub/demodemo/commit/4b81be53772de719b4fe08f3dc348cecc01bb45c.\n",
"The `.update` method also updates the branch the `Content` object is associated with."
]
},
{
"cell_type": "heading",
"level": 1,
"metadata": {},
"source": [
"Modify Multiple Files"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Making a commit that modifies multiple files takes more work.\n",
"I have to stage each change individually as [blobs](http://github3py.readthedocs.org/en/latest/git.html#github3.git.Blob),\n",
"make a new [tree](http://github3py.readthedocs.org/en/latest/git.html#github3.git.Tree) pointing at the new blobs,\n",
"and finally [create a new commit](http://github3py.readthedocs.org/en/latest/repos.html#github3.repos.repo.Repository.create_commit)\n",
"pointed at the new tree.\n",
"This is the procedure outlined in the GitHub API docs [about Git data](http://developer.github.com/v3/git/)."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"repo = repo.refresh()"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 46
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Step one will be to get the current content of some existing files so we can modify them to create new blobs."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"files = repo.contents('/')\n",
"files"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 47,
"text": [
"{'README.md': <Content [README.md]>,\n",
" 'cultural_demodernism.txt': <Content [cultural_demodernism.txt]>,\n",
" 'new_file.md': <Content [new_file.md]>,\n",
" 'newfeature.py': <Content [newfeature.py]>,\n",
" 'postcapitalism.txt': <Content [postcapitalism.txt]>,\n",
" 'test_file.md': <Content [test_file.md]>}"
]
}
],
"prompt_number": 47
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"new_file_content = files['new_file.md'].refresh().decoded.decode('utf-8')\n",
"test_file_content = files['test_file.md'].refresh().decoded.decode('utf-8')"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 59
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"new_file_content"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 60,
"text": [
"'New file!\\n\\nNew line.\\n'"
]
}
],
"prompt_number": 60
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"test_file_content"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 61,
"text": [
"'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'"
]
}
],
"prompt_number": 61
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"new_file_content = new_file_content + '\\nMulti-file commit via the GitHub API!\\n'\n",
"test_file_content = test_file_content + '\\nMulti-file commit via the GitHub API!\\n'"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 62
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"new_file_blob = repo.create_blob(new_file_content, encoding='utf-8')\n",
"test_file_blob = repo.create_blob(test_file_content, encoding='utf-8')\n",
"print(new_file_blob, test_file_blob)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"1f3fd85ad07a8231d3ec53a1d85b9dcab791161b bc3db8ab6c49fe54e57d143b282c33d24e2b4731\n"
]
}
],
"prompt_number": 65
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The new blobs are ready, now to set up a new tree. To create a new tree I'll combine the existing tip tree \n",
"and the new information from the blobs I just created. To get the sha of the existing tip tree I grab a\n",
"[Branch](http://github3py.readthedocs.org/en/latest/repos.html#github3.repos.branch.Branch) object and\n",
"look at its associated commit."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"branch = repo.branch(repo.default_branch)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 68
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"public(branch)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"commit\n",
"etag\n",
"from_json\n",
"last_modified\n",
"links\n",
"name\n",
"ratelimit_remaining\n",
"refresh\n",
"to_json\n"
]
}
],
"prompt_number": 69
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"tree_sha = branch.commit.commit.tree.sha"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 82
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Creating a new tree is done with the \n",
"[Repository.create_tree](http://github3py.readthedocs.org/en/latest/repos.html#github3.repos.repo.Repository.create_tree)\n",
"method."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"tree_data = [{'path': 'new_file.md', 'mode': '100644', 'type': 'blob', 'sha': new_file_blob},\n",
" {'path': 'test_file.md', 'mode': '100644', 'type': 'blob', 'sha': test_file_blob}]\n",
"tree = repo.create_tree(tree_data, tree_sha)\n",
"tree"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 87,
"text": [
"<Tree [57c83a4db05753d333ee4b6277d6a1b830220911]>"
]
}
],
"prompt_number": 87
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And finally I can \n",
"[make a new commit](http://github3py.readthedocs.org/en/latest/repos.html#github3.repos.repo.Repository.create_commit) \n",
"with this new tree. The current branch's commit is used as the parent of the new commit."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"message = 'Modifying multiple files via the GitHub API and github3.py.'\n",
"c = repo.create_commit(message, tree.sha, [branch.commit.sha])\n",
"c"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 90,
"text": [
"<Commit [Matt Davis:eda2a19e23bd52cd50c90b8034763cbc13c3d5fd]>"
]
}
],
"prompt_number": 90
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"c.html_url"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 91,
"text": [
"'https://github.com/jiffyclub/demodemo/commits/eda2a19e23bd52cd50c90b8034763cbc13c3d5fd'"
]
}
],
"prompt_number": 91
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"At this point I've made the commit but the 'master' branch \n",
"[reference](http://git-scm.com/book/en/Git-Internals-The-Refspec)\n",
"is still pointed at the previous commit. \n",
"I'll need to make another API call to \n",
"[update](http://github3py.readthedocs.org/en/latest/git.html#github3.git.Reference.update) \n",
"where the branch is pointed."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"ref = repo.ref('heads/{}'.format(repo.default_branch))"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 92
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"ref.update(c.sha)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 93,
"text": [
"True"
]
}
],
"prompt_number": 93
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And now the branch should be pointed to the new commit."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"branch.links['html']"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 95,
"text": [
"'https://github.com/jiffyclub/demodemo/tree/master'"
]
}
],
"prompt_number": 95
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"That's a lot of jumping through hoops, but for whatever reason it doesn't seem possible\n",
"at the moment to make a commit updating multiple files via simpler API calls.\n",
"I think all that could be wrapped into a function with a definition like:\n",
"\n",
"```\n",
"def multi_commit(files=<a list of paths>, new_content=<list of strings>, branch=<branch name>):\n",
" ...\n",
"```"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [],
"language": "python",
"metadata": {},
"outputs": []
}
],
"metadata": {}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment