Skip to content

jbreeden/mruby-git

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mruby-git (incomplete)

Bindings to libgit2 for MRuby.

Tutorial

The following tutorial was taken from https://libgit2.github.com/docs/guides/101-samples/ and converted to match the MRuby api. (Note: this is a work in progress. The parts present here are the parts that are implemented - though not thoroughly tested).

Initialize the library

The library needs to keep some global state and initialize its dependencies. You must therefore initialize the library before working with it. This is handled for you when mruby-git is loaded, so no need to call it manually.

Git.libgit2_init

Usually you don't need to call the shutdown function as the operating system will take care of reclaiming resources, but if your application uses libgit2 in some areas which are not usually active, you can use

Git.libgit2_shutdown

to ask the library to clean up the global state. The cleanup will be performed once there have been the same number of calls to git_libgit2_shutdown() as there were for git_libgit2_init().

Errors

Any error from libgit2 are raised as Git::Error objects.

puts 'Attempting to open nonexistent repo'
begin
  repo = Git.repository_open('dne')
rescue Git::Error => ex
  puts "#{ex.class}: #{ex.message}"
else
  raise "Expected an error, but didn't catch one."
end

Repositories

Init (Simple)

# With working directory:
puts 'Create a repo with a working directory'
repo = Git.repository_init("sandbox/wd", false)
# …or bare:
puts 'Create a bare repo'
repo = Git.repository_init("sandbox/bare", true)

Init (Options)

opts = Git::RepositoryInitOptions.new

# Customize options
opts.flags |= Git::REPOSITORY_INIT_MKPATH # mkdir as needed to create repo */
opts.description = "My repository has a custom description"

puts "Making a repository in sandbox/no/parent (as with mkdir -p)"
repo = Git.repository_init_ext("sandbox/no/parent", opts)

Clone (Simple)

url = "http://github.com/jbreeden/yargs"
path = "sandbox/yargs"
opt = Git::CloneOptions.new
puts "Cloning yargs repo"
repo = Git.clone(url, path, opt)

Clone (Progress)

(Not yet supported)

# typedef struct { /* … */ } progress_data;
# int fetch_progress(
#             const git_transfer_progress *stats,
#             void *payload)
# {
#   progress_data *pd = (progress_data*)payload;
#   /* Do something with network transfer progress */
# }
# 
# void checkout_progress(
#             const char *path,
#             size_t cur,
#             size_t tot,
#             void *payload)
# {
#   progress_data *pd = (progress_data*)payload;
#   /* Do something with checkout progress */
# }
# 
# /* … */
# progress_data d = {0};
# git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT;
# 
# clone_opts.checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
# clone_opts.checkout_opts.progress_cb = checkout_progress;
# clone_opts.checkout_opts.progress_payload = &d;
# clone_opts.checkout_opts = checkout_opts;
# clone_opts.fetch_opts.callbacks.transfer_progress = fetch_progress;
# clone_opts.fetch_opts.callbacks.payload = &d;
# 
# git_repository *repo = NULL;
# int error = git_clone(&repo, url, path, &clone_opts);

Clone (Custom repo and remote)

(Callbacks not yet supported)

# int create_repsitory(git_repository **out, const char *path, int bare, void *payload)
# {
#     int error;
# 
#     /*
#      * We create the repository ourselves, libgit2 gives us the parameters it would
#      * have used to create the repository. In this case we ignore the path passed
#      * to git_clone() and put it under /tmp/
#      */
#     if ((error = git_repository_init(out, "/tmp/...", bare)) < 0)
#         return error;
# 
#     /* Further customisation of the repository goes here */
# 
#     return 0;
# }
# 
# int create_remote(git_remote **out, git_repository *repo, const char *name, const char *url)
# {
#     int error;
# 
#     /*
#      * Like above, we create the repository based on what libgit2 would have used
#      * (which is what was passed to git_clone. We could use a different refspec
#      * or name.
#      */
#     if ((error = git_remote_create(out, repo, name, url)) < 0)
#         return error;
# 
#     /* Further customisation of the remote goes here */
# 
#     return 0;
# }
# 
# git_repository *repo;
# git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT;
# clone_opts.repository_cb = create_repository;
# clone_opts.remote_cb     = create_remote;
# 
# error = git_clone(&repo, url, path, &clone_opts);

Clone (Mirror)

(Not yet supported)

# int create_remote_mirror(git_remote **out, git_repository *repo, const char *name, const char *url, void *payload)
# {
#     int error;
#     git_remote *remote;
#     git_config *cfg;
#     char *mirror_config;
# 
#     /* Create the repository with a mirror refspec */
#     if ((error = git_remote_create_with_fetchspec(&remote, repo, name, url, "+refs/*:refs/*")) < 0)
#         return error;
# 
#     /* Set the mirror setting to true on this remote  */
#     if ((error = git_repository_config(&cfg, repo)) < 0)
#         return error;
# 
#     if (asprintf(&mirror_config, "remote.%s.mirror", name) == -1) {
#         giterr_set(GITERR_OS, "asprintf failed");
#         git_config_free(cfg);
#         return -1;
#     }
# 
#     error = git_repository_set_bool(cfg, mirror_config, true);
# 
#     free(mirror_config);
#     git_config_free(cfg);
# 
#     return error;
# }
# 
# git_repository *repo = NULL;
# git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT;
# 
# error = git_clone(&repo, url, path, &clone_opts);

Open (Simple)

begin
  repo = Git.repository_open("sandbox/yargs")
rescue Git::Error
  puts "Failed to open yargs repo"
else
  puts "Opened yargs repo #{repo}"
end

Open (Options)

Note that flag options and nullable params (like the ceiling_dirs parameter to repository_open_ext) from the C API are typically optional in the Ruby API.

# Open repository, walking up from given directory to find root.
repo = Git.repository_open_ext("sandbox") # No flags param, defaults to 0

# Open repository in given directory (or fail if not a repository)
begin
  repo = Git.repository_open_ext("sandbox", Git::REPOSITORY_OPEN_NO_SEARCH)
rescue Git::Error
  puts "Error opening non-repo folder as repo - as expected."
else
  raise "Expected an exception, but caught none."
end
# GIT::REPOSITORY_OPEN_CROSS_FS - Unless this flag is set, open will not
#   continue searching across filesystem boundaries (i.e. when `st_dev`
#   changes from the `stat` system call)
puts 'Open repository with "ceiling" directories list to limit walking up'
repo = Git.repository_open_ext("#{Dir.pwd}/sandbox", Git::REPOSITORY_OPEN_CROSS_FS, "#{File.dirname(Dir.pwd)}:/other/dir");

Open (Bare)

A fast way of opening a bare repository when the exact path is known.

repo = Git.repository_open_bare("sandbox/bare")

Find Repository

Check if a given path is inside a repository and return the repository root directory if found. Note that MRuby uses the Git::Buf class to represet struct git_buf. You can use to_s to get the string value from a buffer.

root = Git.repository_discover("./sandbox")
puts "Root of ./sandbox is #{root.to_s}"

Check If Repository

No convenience method for this yet. repository_open will throw. (TODO: Add repository_exists?)

begin
  Git.repository_open("sandbox")
rescue Git::Error => ex
  puts ex.message
end

Objects

SHAs and OIDs

SHA-1 hashes are usually written as 40 characters of hexadecimal. These are converted to a binary representation internally, called git_oid, and there are routines for converting back and forth.

# Convert a SHA to an OID
sha = "4a202b346bb0fb0db7eff3cffeb3c70babbd2045"
oid = Git.oid_fromstr(sha)
puts "Got oid #{oid} (#{oid.class})"

# Make a shortened printable string from an OID
short = Git.oid_tostr_s(oid)[0..6]
puts "Converted Git::Oid to short sha: #{short}"

Lookups

There are four kinds of objects in a Git repository – commits, trees, blobs, and tag annotations. Each type of object has an API for doing lookups.

repo = Git.repository_open("./sandbox/yargs")

begin
  oid = Git.oid_fromstr("a17678a74a41b8a649da1c1782fd63650be4b326")
  commit = Git.commit_lookup(repo, oid)
rescue Git::Error => ex
  puts "Error looking up commit: #{ex.message}"
  raise
end

begin
  oid = Git.oid_fromstr("186fcd23ca6dc239b2d4e3c56981a76c6ad0c0bf")
  tree = Git.tree_lookup(repo, oid)
rescue Git::Error => ex
  puts "Error looking up tree: #{ex.message}"
  raise
end

begin
  oid = Git.oid_fromstr("c815940f2e68aedd6870a5cc79f626f78385041f")
  blob = Git.blob_lookup(repo, oid)
rescue Git::Error => ex
  puts "Error looking up blob: #{ex.message}"
  raise
end

begin
  oid = Git.oid_fromstr("e48f03632fea10a1af70889b39d27dd71c91eee8")
  tag = Git.tag_lookup(repo, oid)
rescue Git::Error => ex
  puts "Error looking up tag: #{ex.message}"
  raise
end

Object Hierarchy

Git::Object acts like a "base class" for all of these types. You can use Git.object_lookup to get an object regardless of type.

begin
  oid = Git.oid_fromstr("a17678a74a41b8a649da1c1782fd63650be4b326")
  commit = Git.object_lookup(repo, oid)
  puts "Object lookup for commit returned a #{commit.class}" # => Git::Commit
rescue Git::Error => ex
  puts "Error looking up commit: #{ex.message}"
  raise
end

begin
  oid = Git.oid_fromstr("186fcd23ca6dc239b2d4e3c56981a76c6ad0c0bf")
  tree = Git.object_lookup(repo, oid)
  puts "Object lookup for tree returned a #{tree.class}" # => Git::Tree
rescue Git::Error => ex
  puts "Error looking up tree: #{ex.message}"
  raise
end

begin
  oid = Git.oid_fromstr("c815940f2e68aedd6870a5cc79f626f78385041f")
  blob = Git.object_lookup(repo, oid)
  puts "Object lookup for blob returned a #{blob.class}" # => Git::Blob
rescue Git::Error => ex
  puts "Error looking up blob: #{ex.message}"
  raise
end

begin
  oid = Git.oid_fromstr("e48f03632fea10a1af70889b39d27dd71c91eee8")
  tag = Git.object_lookup(repo, oid)
  puts "Object lookup for tag returned a #{tag.class}" # => Git::Tag
rescue Git::Error => ex
  puts "Error looking up tag: #{ex.message}"
  raise
end

Blobs

Content

blob = Git.object_lookup(repo, Git.oid_fromstr("c815940f2e68aedd6870a5cc79f626f78385041f"))
rawsize = Git::blob_rawsize(blob)
rawcontent = Git::blob_rawcontent(blob)
puts "Raw content of blob: \n#{rawcontent}"

filtered_content = Git.blob_filtered_content(
  blob,                # blob
  "yargs.rb",          # path (for attribute-based filtering)
  true)                # check if binary?
puts "Filtered content of blob: \n#{filtered_content}"

Create


oid = Git.blob_create_fromworkdir(repo, "README.md")
puts oid # => f561043483649a283f67d02ff99d403fa86cdb88

oid = Git.blob_create_fromdisk(repo, "/etc/hosts")
puts oid #=> e30a9d040bada9de35ddda5f8a5f6427be7b387f

str = "# Hello there!"
oid = Git.blob_create_frombuffer(repo, str)
puts oid # => fad4d167a9db3d88ae591b2b4aad3bd7e3e9fe94

Trees

A tree object in libgit2 is more like a directory. It can represent a directory tree by containing references to other trees.

Lookups

Each commit has a tree, you can look them up by OID:

oid = Git.oid_fromstr("a17678a74a41b8a649da1c1782fd63650be4b326")
commit = Git.commit_lookup(repo, oid)
tree = Git.commit_tree(commit)
puts "Tree: #{tree}"

Trees can contain trees:

entry = Git.tree_entry_byindex(tree, 0)
if Git.tree_entry_type(entry) == Git::OBJ_TREE
  subtree = Git.tree_lookup(repo, Git.tree_entry_id(entry))
end

Tree Entries

tree = Git.revparse_single(repo, "HEAD^{tree}")
puts "Tree: #{tree}"

count = Git.tree_entrycount(tree)
puts "Count: #{count}"

entry = Git.tree_entry_byindex(tree, 0)
puts "Entry: #{entry}"

name = Git.tree_entry_name(entry) # filename
puts "Name: #{name}"

objtype = Git.tree_entry_type(entry) # blob or tree
puts "Object type: #{objtype}"

mode = Git.tree_entry_filemode(entry) # *NIX filemode
puts "Mode: #{mode}"

# In C, you have to be careful to free this one,
# unlike the results from most lookups. mruby-git
# handles that for you.
entry = Git.tree_entry_bypath(tree, "README.md")

Walking

(Not yet supported)

#typedef struct { /* … */ } walk_data;
#
#int walk_cb(const char *root,
#            const git_tree_entry *entry,
#            void *payload)
#{
#  walk_data *d = (walk_data*)payload;
#  /* … */
#}
#
#git_object *obj = NULL;
#int error = git_revparse_single(&obj, repo, "HEAD^{tree}");
#git_tree *tree = (git_tree *)obj;
#
#walk_data d = {0};
#error = git_tree_walk(tree, GIT_TREEWALK_PRE, walk_cb, &d);

Treebuilder

Since trees in git are immutable we need a mechanism to build them. This method in libgit2 is the treebuilder. Just like the tree object, the treebuilder object represents a single directory containing other objects.

bld = Git.treebuilder_new(repo)

# Add some entries
obj = Git.revparse_single(repo, "HEAD:README.md")
Git.treebuilder_insert(bld,
                       "README.md",        # filename
                       Git.object_id(obj), # OID
                       Git::FILEMODE_BLOB)  # mode

obj = Git.revparse_single(repo, "v1.0.0:yargs.rb")
Git.treebuilder_insert(bld,
                       "d.c",
                       Git.object_id(obj),
                       Git::FILEMODE_BLOB)

oid = Git.treebuilder_write(bld)
puts "Wrote tree #{oid}"

Commits

Lookups

obj = Git.revparse_single(repo, "HEAD")
commit = Git.commit_lookup(repo, Git.object_id(obj))
puts "Commit: #{commit}"

Properties

puts "Commit id:  #{Git.commit_id(commit)}"
puts "Commit message_encoding:  #{Git.commit_message_encoding(commit)}"
puts "Commit message:  #{Git.commit_message(commit)}"
puts "Commit summary:  #{Git.commit_summary(commit)}"
puts "Commit time:  #{Git.commit_time(commit)}"
puts "Commit time_offset:  #{Git.commit_time_offset(commit)}"
puts "Commit committer:  #{Git.commit_committer(commit)}"
puts "Commit author:  #{Git.commit_author(commit)}"
puts "Commit raw_header:  \n#{Git.commit_raw_header(commit)}"
puts "Commit tree_id:  #{Git.commit_tree_id(commit)}"

Parents

count = Git.commit_parentcount(commit)
(0..(count - 1)).each do |i|
  nth_parent_id = Git.commit_parent_id(commit, i)
  nth_parent = Git.commit_parent(commit, i)
  puts "Parent #{i}: #{nth_parent}"
end

nth_ancestor = Git.commit_nth_gen_ancestor(commit, 2)
puts "2nd ancestory: #{nth_ancestor}"

Create

my_signature = Git.signature_now("Me", "me@example.com")

parents = [Git.revparse_single(repo, "HEAD")]

oid = Git.commit_create(
  repo,
  "HEAD",                      # name of ref to update
  my_signature,                          # author
  my_signature,                          # committer
  "UTF-8",                     # message encoding
  "Flooberhaul the whatnots",  # message
  tree,                        # root tree
  parents)                     # parents
puts "Created commit #{oid}"

References

Lookups (full name)

ref = Git.reference_lookup(repo, "refs/heads/master") puts "refs/heads/master: #{ref}"

Lookups (short name)

ref = Git.reference_dwim(repo, "HEAD") # "master" isn't working... TODO
puts "HEAD^: #{ref}"

Lookups (resolved)

Get the object pointed to by a symbolic reference (or a chain of them).

oid = Git.reference_name_to_id(repo, "HEAD")
puts "HEAD: #{oid}"

Listing

reflist = Git.reference_list(repo)
puts "Reflist: #{reflist}"
puts "Reflist Strings: #{reflist.strings}"

Foreach (refs)

(Not yet supported)

# typedef struct { /* … */ } ref_data;
# 
# int each_ref_cb(git_reference *ref, void *payload)
# {
#   ref_data *d = (ref_data*)payload;
#   /* … */
# }
# 
# ref_data d = {0};
# int error = git_reference_foreach(repo, each_ref_cb, &d);

Foreach (names)

(Not yet supported)

#typedef struct { /* … */ } ref_data;
#
#int each_name_cb(const char *name, void *payload)
#{
#  ref_data *d = (ref_data*)payload;
#  /* … */
#}
#
#ref_data d = {0};
#int error = git_reference_foreach_name(repo, each_name_cb, &d);

Foreach (glob)

(Not yet supported)

#typedef struct { /* … */ } ref_data;
#
#int each_name_cb(const char *name, void *payload)
#{
#  ref_data *d = (ref_data*)payload;
#  /* … */
#}
#
#ref_data d = {0};
#int error = git_reference_foreach_glob(repo, "refs/remotes/*", each_name_cb, &d);

#### Iterator (all)
```Ruby
iter = Git.reference_iterator_new(repo)

# Git.reference_next() return nil after last element (any errors are raised)
while ref = Git.reference_next(iter)
  puts "Ref iteration: #{ref}"
end
puts "Done iterating"

Iterator (glob)

iter = Git.reference_iterator_glob_new(repo, "refs/heads/*")

while ref_name = Git.reference_next_name(iter)
  puts "Reference Name Iteration: #{ref_name}"
end

Create (direct)

oid = Git.object_id(Git.revparse_single(repo, "HEAD"))
ref = Git.reference_create(
  repo,
  "refs/heads/test/ing", # name
  oid,                 # target
  true,                # force?
)                  # message param omitted (would be a string for the reflog)
puts "Made new ref: #{ref}"
puts "Made `direct` ref pointing to HEAD: #{ref}"

Create (symbolic)

ref = Git.reference_symbolic_create(
      repo,
      "refs/heads/symbolic",     # name
      "refs/heads/master",       # target
      true,                      # force?
      "Making a symbolic ref")   # Reflog message
puts "Made new symbolic ref: #{ref}"

Tags

Lookups (annotations)

tag = Git.tag_lookup(repo, Git.object_id(Git.revparse_single(repo, "v1.0.0")))
puts "Tag: #{tag}"

Listing (all)

tags = Git.tag_list(repo)
puts "Tags: #{tags.strings}"

Listing (glob)

matching_tags = Git.tag_list_match("v1.*", repo)
puts "Matching tag: #{matching_tags.strings}"

Foreach

(Not yet supported)

#typedef struct { /* … */ } tag_data;
#
#int each_tag(const char *name, git_oid *oid, void *payload)
#{
#  tag_data *d = (tag_data*)payload;
#  /* … */
#}
#
#tag_data d = {0};
#int error = git_tag_foreach(repo, each_tag, &d);

Annotation Properties

puts "Tag target_id: #{Git.tag_target_id(tag)}"
puts "Tag target_type: #{Git.tag_target_type(tag)}"
puts "Tag name: #{Git.tag_name(tag)}"
puts "Tag tagger: #{Git.tag_tagger(tag)}"
puts "Tag message: #{Git.tag_message(tag)}"

Create (lightweight)

target = Git.revparse_single(repo, "HEAD^{commit}")
oid = Git.tag_create_lightweight(
      repo,       # repository
      "v2.3.4",   # name
      target,     # target
      false)      # force?

Create (annotated)

target = Git.revparse_single(repo, "HEAD^{commit}")
tagger = Git.signature_now(
      "Ben Straub",       # name
      "bs@github.com")    # email
oid = Git.tag_create(
      repo,               # repo
      "v2.3.5",           # name
      target,             # target
      tagger,             # name/email/timestamp
      "Released 10/5/11", # message
      false)              # force?
puts "Created tag: #{oid}"

Peeling

obj = Git.tag_peel(tag)
puts "Peeled tag: #{obj}"

Index

Loading

# Each repository owns an index
idx = Git.repository_index(repo)
puts "Index: #{idx}"

# Or you can open it by path
idx = Git.index_open("sandbox/yargs/.git/index")
puts "Index by path: #{idx}"

Creating (in-memory)

In-memory indexes cannot be saved to disk, but can be useful for creating trees.

in_memory_idx = Git.index_new
puts "New index: #{in_memory_idx}"

Disk

# Make the in-memory index match what's on disk
Git.index_read(idx, true)

# Write the in-memory index to disk
Git.index_write(idx)

Trees

Note that all tree operations work recursively. For example, git_index_read_tree will replace not only the root directory, but all subdirectory contents as well.

# Overwrite the index contents with those of a tree
idx = Git.repository_index(repo)
tree = Git.revparse_single(repo, "HEAD~^{tree}")
Git.index_read_tree(idx, tree)

# Write the index contents to the ODB as a tree
new_tree_id = Git.index_write_tree(idx)

# In-memory indexes can write trees to any repo
new_tree_id = Git.index_write_tree_to(idx, other_repo)

Entries

# Access by index
count = Git.index_entrycount(idx)
(0...count).each do |i|
  entry = Git.index_get_byindex(idx, i)
  puts "Index entry: #{entry}"
end

# Access by path
entry = Git.index_get_bypath(
        idx,                # index
        "path/to/file.rb",  # path
        0)                  # stage

Conflicts

if (Git.index_has_conflicts(idx))
  # If you know the path of a conflicted file
  ancestor, ours, theirs = Git.index_conflict_get(idx, "path/to/file.cs")

  # Or, iterate through all conflicts
  iter = Git.index_conflict_iterator_new(idx)
  loop {
    ancestor, ours, theirs = Git.index_conflict_next(iter)
    break if ancestor.nil? # || ours.nil? || theirs.nil?
    puts "Conflict entry: #{ancestor}, #{ours} #{theirs}"
    
    # Mark this conflict as resolved
    Git.index_conflict_remove(idx, ours.path)
  }
  Git.index_conflict_iterator_free(iter)
end

Add & Remove

About

MRuby bindings to libgit2

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published