GitHub’s Git Data API

This is a tutorial on how to programatically create single commit for multiple file changes, which is not obvious from Octokit API.

GitHub has a low-level Git Data API. You can do basically everything with Git via this powerful API!

In this tutorial, I am going to walk you through how to use this API with Octokit to change files in one single commit in a new branch and send a Pull Request.

Suppose we want to send a Pull Request for https://github.com/JuanitoFatas/git-playground with these changes:

  • append bar to file foo
  • append baz to file bar
  • in one commit with the message "Update foo & bar in a new topic branch "update-foo-and-bar".

This is how you could do it:

$ gem install octokit

Get an access token, and open irb with octokit required, then create an Octokit client with your token:

$ irb -r octokit

> client = Octokit::Client.new(access_token: "<your 40 char token>")

We also prepare two variables to be used later, the repo name and new branch name:

repo = "JuanitoFatas/git-playground"
new_branch_name = "update-foo-and-bar"

First, let's get the base branch (in this case, master branch) SHA1, so that we can branch from master.

We can use the Octokit#refs method to get the base branch SHA1:

master = client.refs(repo).find do |reference|
  "refs/heads/master" == reference.ref
end

base_branch_sha = master.object.sha

And creates a new branch from base branch via Octokit#create_ref method:

new_branch = client.create_ref(repo, "heads/#{new_branch_name}", base_branch_sha)

The tricky part here is that you need to prefix your new branch name with "heads/".

First let's use Octokit#contents method with the SHA1 to get existing foo and bar files' content.

foo = client.contents repo, path: "foo", sha: base_branch_sha
bar = client.contents repo, path: "foo", sha: base_branch_sha

Contents on GitHub API is Base64-encoded, we need to decode and append "bar" to foo file, "baz" to bar file respectively:

require "base64"

# path => new content
new_contents = {
  "foo" => Base64.decode64(foo.content) + "bar",
  "bar" => Base64.decode64(foo.content) + "baz"
}

Creates a new tree with our new files (blobs), the new blob can be created via (Octokit#create_blob method). This new tree will be part of our new “tree”.

new_tree = new_contents.map do |path, new_content|
  Hash(
    path: path,
    mode: "100644",
    type: "blob",
    sha: client.create_blob(repo, new_content)
  )
end

Get the current commit first via Octokit#git_commit method:

commit = client.git_commit(repo, new_branch["object"]["sha"])

Note that this method is not the same as Octokit#commit method. git_commit is from the low-level Git Data API, while commit is using the Commits API.

Now we get the commit object, we can retrieve the tree:

tree = commit["tree"]

Creates a new tree by Octokit#create_tree method with the blobs object we created earlier:

new_tree = client.create_tree(repo, new_tree, base_tree: tree["sha"])

The base_tree argument here is important. Pass in this option to update an existing tree with new data.

Now our new tree is ready, we can add a commit onto it:

commit_message = "Update foo & bar"
new_commit = client.create_commit(repo, commit_message, new_tree["sha"], commit["sha"])

Finally, update the reference via Octokit#update_ref method on the new branch:

client.update_ref(repo, "heads/#{new_branch_name}", new_commit["sha"])

Creates a new Pull Request via Octokit#create_pull_request method:

title = "Update foo and bar"
body = "This Pull Request appends foo with `bar`, bar with `baz`."
client.create_pull_request(repo, "master", new_branch_name, title, body)

That's it! :sparkles: See the result here.

Now you can do basically everything with Git via GitHub's Git Data API!

May the Git Data API be with you.

Thanks for reading!