Last Updated: February 25, 2016
·
3.091K
· mojavelinux

Add a git submodule using Rugged

This tip demonstrates how to build a commit using Rugged that will add a submodule to a git repository.

Rugged is a library that provides Ruby API bindings for libgit2, a C implementation of the git operations. Rugged is quickly replacing grit and similar Ruby libraries because it performs operations on the git repository using direct API calls instead of calling git as a system command.

As of version 0.19.0, Rugged does not provide an API for adding a submodule to a git repository. However, it does provide the low-level features necessary to build a submodule reference manually. You just need to understand how a submodule is tracked in the git repository.

We'll start by creating a repository without using Rugged to establish a starting point.

git_email = 'octocat@github.com'
git_name = 'The Octocat'
repo_name = 'sample-repo'

Dir.mkdir repo_name
Dir.chdir repo_name do
  File.open 'README.adoc', 'w' do |f|
    f.write %(= README\n\nYou will read me and be informed.)
  end
  `git init .`
  `git add README.adoc`
  `git commit -m "add README" --author "#{git_name} <#{git_email}>" README.adoc`
end

You can inspect the repository to see that it has one file and one commit.

We'll now use Rugged to add a .gitmodules file and another special file that indicates which commit to use from the submodule's repository.

require 'rugged'

submodule_path = 'example-submodule'
submodule_url = 'https://github.com/octocat/Spoon-Knife.git'
submodule_last_commit = 'd0dd1f61b33d64e29d8bc1372a94ef6a2fee76a9'

# (1)
repo = Rugged::Repository.new repo_name

# (2)
repo.checkout 'refs/heads/master'

# (3)
index = repo.index

# (4)
File.open File.join(repo.workdir, '.gitmodules'), 'w' do |f|
  f.write %([submodule "#{submodule_path}"]
\tpath = #{submodule_path}
\turl = #{submodule_url})
end
index.add path: '.gitmodules',
  oid: (Rugged::Blob.from_workdir repo, '.gitmodules'),
  mode: 0100644

# (5)
index.add path: submodule_path,
  oid: submodule_last_commit,
  mode: 0160000

# (6)
commit_tree = index.write_tree repo

# (7)
index.write

# (8)
commit_author = { email: git_email, name: git_name, time: Time.now }
Rugged::Commit.create repo,
  author: commit_author,
  committer: commit_author,
  message: 'Adding example submodule',
  parents: [repo.head.target],
  tree: commit_tree,
  update_ref: 'HEAD'

Here's what this code is doing:

  • (1) Load up the repository in Rugged
  • (2) Make sure we're on the right branch (e.g., refs/heads/master)
  • (3) Retrieve a reference to the repository's index for this branch
  • (4) Build the .gitmodules file, write it to disk and add it to the repository's index
  • (5) Add the special reference file that establishes the subproject commit (i.e., which commit to use from the submodule's repository)
  • (6) Write the changes made to the index to the git repository (i.e., the git database)
  • (7) Sync the index and the working directory (does not modify the working directory)
  • (8) Create a new commit and update HEAD to point to it

The most important step in this process is #5, which creates the file that establishes the subproject commit.

When you look at the diff of the last commit, you'll see the following entry:

diff --git a/example-submodule b/example-submodule
new file mode 160000
index 0000000..d0dd1f6
--- /dev/null
+++ b/example-submodule
@@ -0,0 +1 @@
+Subproject commit d0dd1f61b33d64e29d8bc1372a94ef6a2fee76a9

You'll notice that the mode used is 0160000. This is a special mode that tells git that this is not a regular file, but rather a subproject (i.e., submodule) commit reference. Although it appears that git has created a file containing a line that begins with "Subproject commit", there is no file with that content on disk. That content is auto-generated and part of the internal object that stores the submodule information.

Here's how the .gitmodule file looks when we're done.

[submodule "example-submodule"]
    path = example-submodule
    url = https://github.com/octocat/Spoon-Knife.git

Now clone this repository with recursion enable into an adjacent folder.

$ git clone --recursive sample-repo sample-repo-clone

You should see that the clone operation successfully retrieves the submodule repository.