Fix Issue #70 : implement branch API #72

Closed
wants to merge 3 commits into
from
Jump to file or symbol
Failed to load files and symbols.
+320 −0
Diff settings

Always

Just for now

View
@@ -5,4 +5,6 @@ test/fixtures/testrepo.git
ext/rugged/vendor/libgit2-dist/
ext/rugged/vendor/mkmf.log
ext/rugged/libgit2_embed.a
+lib/rugged/rugged.so
+vendor/*
.yardoc
View
@@ -39,6 +39,7 @@
#include <assert.h>
#include <git2.h>
#include <git2/odb_backend.h>
+#include <git2/branch.h>
#define CSTR2SYM(s) (ID2SYM(rb_intern((s))))
@@ -99,6 +100,20 @@ static inline int rugged_parse_bool(VALUE boolean)
return boolean ? 1 : 0;
}
+static inline int rugged_parse_branch_type(VALUE type) {
+ ID t;
+
+ Check_Type(type, T_SYMBOL);
+ t = SYM2ID(type);
+ if ( t == rb_intern("local")) {
+ return GIT_BRANCH_LOCAL;
+ } else if (t == rb_intern("remote")) {
+ return GIT_BRANCH_REMOTE;
+ } else {
+ rb_raise(rb_eTypeError, "Branch type should be either :local or :remote");
+ }
+}
+
/* support for string encodings in 1.9 */
#ifdef HAVE_RUBY_ENCODING_H
# define rugged_str_new(str, len, enc) rb_enc_str_new(str, len, enc)
View
@@ -540,6 +540,161 @@ static VALUE rb_git_repo_set_workdir(VALUE self, VALUE rb_workdir)
return Qnil;
}
+/*
+ * call-seq:
+ * repo.create_branch( name, target_oid, force = false)
+ *
+ * Creates a branch named +name+ in +repo+, pointing at +target_oid+
+ * Optionaly it orverwrite +branch+ if it is already existing
+ * and that the creation has been forced (+force+ set to true)
+ *
+ * Note that +target_oid+ should point either to a commit or
+ * to a tag that can be dereferenced.
+ */
+static VALUE rb_git_repo_create_branch(int argc, VALUE* argv, VALUE self){
+ VALUE rb_name, rb_target, rb_force;
+ git_repository *repo;
+ git_oid oid, out_oid;
+ size_t length;
+ git_otype otype;
+ git_object *obj;
+ int error, force = 0;
+
+ rb_scan_args(argc, argv, "21", &rb_name, &rb_target, &rb_force);
+
+ Data_Get_Struct(self, git_repository, repo);
+ Check_Type(rb_name, T_STRING);
+ Check_Type(rb_target, T_STRING);
+
+ if (!NIL_P(rb_force))
+ force = rugged_parse_bool(rb_force);
+
+ error = git_oid_fromstr(&oid, StringValueCStr(rb_target));
+ rugged_exception_check(error);
+
+ error = git_object_lookup(&obj, repo, &oid, GIT_OBJ_ANY);
+ rugged_exception_check(error);
+
+ /*
+ * either a commit or a tag
+ */
+ otype = git_object_type(obj);
+ if ((otype != GIT_OBJ_COMMIT) && (otype != GIT_OBJ_TAG)) {
+ git_object_free(obj);
+ rb_raise(rb_eTypeError, "Target is neither a commit nor a tag");
+ }
+
+ error = git_branch_create(&out_oid, repo, StringValueCStr(rb_name), obj, force);
+ /*
+ * Object is useless now (in all cases) so let's free it.
+ * Note that git_object_free does not call giterr_set, so no chance
+ * it changes the error rugged_exception_check is looking at
+ */
+ git_object_free(obj);
+ rugged_exception_check(error);
+
+ return rugged_create_oid(&oid);
+}
+
+/*
+ * call-seq:
+ * repo.move_branch( old_branch, new_branch, force = false)
+ *
+ * Renames +old_branch+ to +new_branch+. If +new_branch+ exists and +force+
+ * is true, move +old_branch+ to +new_branch+, overwriting it.
+ */
+static VALUE rb_git_repo_move_branch(int argc, VALUE* argv, VALUE self){
+ VALUE rb_old_branch, rb_new_branch, rb_force;
+ git_repository *repo;
+ int error, force = 0;
+
+ rb_scan_args(argc, argv, "21", &rb_old_branch, &rb_new_branch, &rb_force);
+
+ Data_Get_Struct(self, git_repository, repo);
+ Check_Type(rb_old_branch, T_STRING);
+ Check_Type(rb_new_branch, T_STRING);
+
+ if (!NIL_P(rb_force))
+ force = rugged_parse_bool(rb_force);
+
+ error = git_branch_move( repo, StringValueCStr(rb_old_branch), StringValueCStr(rb_new_branch), force);
+ rugged_exception_check(error);
+
+ return Qnil;
+}
+
+/*
+ * call-seq:
+ * repo.delete_branch( name, type = :local)
+ *
+ * Deletes the branch +name+.
+ * +type+ of the branch can be either +:local+(default) or +:remote+
+ *
+ */
+static VALUE rb_git_repo_delete_branch(int argc, VALUE* argv, VALUE self){
+ VALUE rb_name, rb_type;
+ git_repository *repo;
+ int error, type = GIT_BRANCH_LOCAL;
+
+ rb_scan_args(argc, argv, "11", &rb_name, &rb_type);
+
+ Data_Get_Struct(self, git_repository, repo);
+ Check_Type(rb_name, T_STRING);
+
+ if (!NIL_P(rb_type)) {
+ Check_Type(rb_type, T_SYMBOL);
+ type = rugged_parse_branch_type(rb_type);
+ }
+
+ error = git_branch_delete( repo, StringValueCStr(rb_name), type);
+ rugged_exception_check(error);
+
+ return Qnil;
+}
+
+/*
+ * call-seq:
+ * repo.each_branch( type = nil, &block )
+ *
+ * Calls the block for each branch of the specified +type+, passing it the
+ * branch name.
+ *
+ * +type+ can be:
+ * * :local only local branches are considered
+ * * :remote only remote branches are considered
+ * * nil local and remote branches are considered
+ *
+ */
+static VALUE rb_git_repo_each_branch(int argc, VALUE *argv, VALUE self) {
+ VALUE rb_type;
+ git_repository *repo;
+ git_strarray branches;
+ unsigned int i;
+ int error, type = GIT_BRANCH_LOCAL | GIT_BRANCH_REMOTE;
+
+ rb_scan_args(argc, argv, "01", &rb_type);
+
+ if (!rb_block_given_p())
+ rb_raise(rb_eArgError, "Expected block");
+
+ Data_Get_Struct(self, git_repository, repo);
+
+ if (!NIL_P(rb_type)) {
+ Check_Type(rb_type, T_SYMBOL);
+ type = rugged_parse_branch_type(rb_type);
+ }
+
+ error = git_branch_list(&branches, repo, type);
+ rugged_exception_check(error);
+
+ for (i = 0; i < branches.count; ++i) {
+ rb_yield(rb_str_new2(branches.strings[i]));

This comment has been minimized.

Show comment Hide comment
@tenderlove

tenderlove Jun 20, 2012

Contributor

Does this need to take in to account repo encoding? rb_str_new2 is always going to return strings tagged as ASCII-8BIT. I can fix this after we merge, but I need to understand the encoding rules for libgit2.

@tenderlove

tenderlove Jun 20, 2012

Contributor

Does this need to take in to account repo encoding? rb_str_new2 is always going to return strings tagged as ASCII-8BIT. I can fix this after we merge, but I need to understand the encoding rules for libgit2.

This comment has been minimized.

Show comment Hide comment
@sleeper

sleeper Jun 20, 2012

As far as I remember, the branches and reference names are treated internally as ASCII strings (I.e. a pointer of chars). Do not know which policy should be applied in rugged.

@sleeper

sleeper Jun 20, 2012

As far as I remember, the branches and reference names are treated internally as ASCII strings (I.e. a pointer of chars). Do not know which policy should be applied in rugged.

This comment has been minimized.

Show comment Hide comment
@tenderlove

tenderlove Jun 20, 2012

Contributor

It seems possible to use unicode characters for branch names:

[aaron@higgins rugged (こんにちは)]$ git status
# On branch こんにちは
nothing to commit (working directory clean)
[aaron@higgins rugged (こんにちは)]$

Returning ASCII-8BIT is safe for now, but we really need to figure out what the encoding policy is for git internals like this. Is it always unicode? Does it depend on the filesystem? Or something else?

@tenderlove

tenderlove Jun 20, 2012

Contributor

It seems possible to use unicode characters for branch names:

[aaron@higgins rugged (こんにちは)]$ git status
# On branch こんにちは
nothing to commit (working directory clean)
[aaron@higgins rugged (こんにちは)]$

Returning ASCII-8BIT is safe for now, but we really need to figure out what the encoding policy is for git internals like this. Is it always unicode? Does it depend on the filesystem? Or something else?

This comment has been minimized.

Show comment Hide comment
@tenderlove

tenderlove Jun 20, 2012

Contributor

According to the git log page I think we're supposed to assume strings are UTF-8 unless someone configures with a different output encoding. Look for the Discussion section.

@tenderlove

tenderlove Jun 20, 2012

Contributor

According to the git log page I think we're supposed to assume strings are UTF-8 unless someone configures with a different output encoding. Look for the Discussion section.

This comment has been minimized.

Show comment Hide comment
@vmg

vmg Jun 20, 2012

Member

We've been working with Core Git and MSysGit to add some consistency to Unicode handling in Git. The result is as follows: UTF-8 everywhere. All the time. We do not consider any output encodings anymore because this would be a disaster under Win32. Anybody using something that is not UTF-8 is wrong and stupid and ugly and should feel bad about it.

The only exception to this are Commit Messages, which have support for an encoding line to define a custom encoding. We already handle those properly in 1.9.x

@vmg

vmg Jun 20, 2012

Member

We've been working with Core Git and MSysGit to add some consistency to Unicode handling in Git. The result is as follows: UTF-8 everywhere. All the time. We do not consider any output encodings anymore because this would be a disaster under Win32. Anybody using something that is not UTF-8 is wrong and stupid and ugly and should feel bad about it.

The only exception to this are Commit Messages, which have support for an encoding line to define a custom encoding. We already handle those properly in 1.9.x

This comment has been minimized.

Show comment Hide comment
@sleeper

sleeper Jun 20, 2012

Excuse my question, but what does it means regarding libgit2 (and then rugged) ?

As far as I remember the commit that I proposed, regarding inconsistencies on branche names, was playing with reference and branch names and was manipulating only "standard" C string (i.e. NULL-terminated array of chars).

@sleeper

sleeper Jun 20, 2012

Excuse my question, but what does it means regarding libgit2 (and then rugged) ?

As far as I remember the commit that I proposed, regarding inconsistencies on branche names, was playing with reference and branch names and was manipulating only "standard" C string (i.e. NULL-terminated array of chars).

This comment has been minimized.

Show comment Hide comment
@vmg

vmg Jun 20, 2012

Member

UTF-8 strings are NULL-terminated arrays of characters. They require no special handling.

@vmg

vmg Jun 20, 2012

Member

UTF-8 strings are NULL-terminated arrays of characters. They require no special handling.

This comment has been minimized.

Show comment Hide comment
@arthurschreiber

arthurschreiber Jun 20, 2012

Member

Use rugged_str_new2 instead of rb_str_new2 and add rb_utf8_encoding() as a second parameter.

@arthurschreiber

arthurschreiber Jun 20, 2012

Member

Use rugged_str_new2 instead of rb_str_new2 and add rb_utf8_encoding() as a second parameter.

This comment has been minimized.

Show comment Hide comment
@sleeper

sleeper Jun 20, 2012

OK, but does it work, in C, with literal strings ?
Meaning, I've this code:

git__strdup(branch_name +strlen(GIT_REFS_HEADS_DIR))

and

#define GIT_REFS_HEADS_DIR GIT_REFS_DIR "heads/"

if branch_name points to an "UTF-8" string, I'm not sure it will work (i.e. the literal strings in C are ASCII-8 no ?)

@sleeper

sleeper Jun 20, 2012

OK, but does it work, in C, with literal strings ?
Meaning, I've this code:

git__strdup(branch_name +strlen(GIT_REFS_HEADS_DIR))

and

#define GIT_REFS_HEADS_DIR GIT_REFS_DIR "heads/"

if branch_name points to an "UTF-8" string, I'm not sure it will work (i.e. the literal strings in C are ASCII-8 no ?)

This comment has been minimized.

Show comment Hide comment
@sleeper

sleeper Jun 20, 2012

@arthurschreiber OK ! I didn't know about rugged_str_new2 ... but I had a look at rugget_commit.c with good examples ;)

@sleeper

sleeper Jun 20, 2012

@arthurschreiber OK ! I didn't know about rugged_str_new2 ... but I had a look at rugget_commit.c with good examples ;)

This comment has been minimized.

Show comment Hide comment
@sleeper

sleeper Jun 20, 2012

And taking inspiration from rugged_reference.c what about something like:

rb_yield(rb_str_new2(rugged_str_new2(branches.strings[i], NULL));
@sleeper

sleeper Jun 20, 2012

And taking inspiration from rugged_reference.c what about something like:

rb_yield(rb_str_new2(rugged_str_new2(branches.strings[i], NULL));

This comment has been minimized.

Show comment Hide comment
@arthurschreiber

arthurschreiber Jun 20, 2012

Member

No, I think you got me wrong. What you need to do is:

rb_yield(rugged_str_new2(branches.strings[i], rb_utf8_encoding()));
@arthurschreiber

arthurschreiber Jun 20, 2012

Member

No, I think you got me wrong. What you need to do is:

rb_yield(rugged_str_new2(branches.strings[i], rb_utf8_encoding()));

This comment has been minimized.

Show comment Hide comment
@sleeper

sleeper Jun 21, 2012

Yep, I understood.
I was taking example on rugged_reference.c, for example, the function that return the reference name, which indeed was using NULL for the encoding.
I just checked, and if I understand, looking at ruby sources, this mainly means, returning ASCII ...

Is this normal/coherent ? Why not using rugged_str_ascii instead of rugged_str_new2 with NULL for 2 param ? I'm a little bit lost ;)

@sleeper

sleeper Jun 21, 2012

Yep, I understood.
I was taking example on rugged_reference.c, for example, the function that return the reference name, which indeed was using NULL for the encoding.
I just checked, and if I understand, looking at ruby sources, this mainly means, returning ASCII ...

Is this normal/coherent ? Why not using rugged_str_ascii instead of rugged_str_new2 with NULL for 2 param ? I'm a little bit lost ;)

+ }
+
+ git_strarray_free(&branches);
+
+ return Qnil;
+}
/*
* call-seq:
* Repository.discover(path = nil, across_fs = true) -> repository
@@ -713,6 +868,11 @@ void Init_rugged_repo()
rb_define_method(rb_cRuggedRepo, "config", rb_git_repo_get_config, 0);
rb_define_method(rb_cRuggedRepo, "config=", rb_git_repo_set_config, 1);
+ rb_define_method(rb_cRuggedRepo, "create_branch", rb_git_repo_create_branch, -1);
+ rb_define_method(rb_cRuggedRepo, "move_branch", rb_git_repo_move_branch, -1);
+ rb_define_method(rb_cRuggedRepo, "delete_branch", rb_git_repo_delete_branch, -1);
+ rb_define_method(rb_cRuggedRepo, "each_branch", rb_git_repo_each_branch, -1);
+
rb_define_method(rb_cRuggedRepo, "bare?", rb_git_repo_is_bare, 0);
rb_define_method(rb_cRuggedRepo, "empty?", rb_git_repo_is_empty, 0);
View
@@ -0,0 +1,143 @@
+require "test_helper"
+
+context "Rugged::Repository branch stuff" do
+ setup do
+ @path = File.dirname(__FILE__) + '/fixtures/testrepo.git'
+ @repo = Rugged::Repository.new(@path)
+ end
+
+ teardown do
+ FileUtils.remove_entry_secure(@path + '/refs/heads/foo_branch', true)
+ FileUtils.remove_entry_secure(@path + '/refs/heads/foo', true)
+ end
+
+ test "can create a branch" do
+ ref = Rugged::Reference.lookup(@repo, "refs/heads/master")
+ @repo.create_branch("foo_branch", ref.target )
+ branches = @repo.refs(/foo_branch/)
+ assert_equal branches.size, 1
+ assert_equal branches[0].target, ref.target
+ end
+
+ test "should check the branch name when creating" do
+ assert_raise(TypeError){@repo.create_branch(1, "c0004" )}
+ end
+
+ test "should check the target when creating" do
+ assert_raise(TypeError){@repo.create_branch("foo_branch", 1)}
+ end
+
+ test "should check the target is valid when creating" do
+ assert_raise(Rugged::InvalidError){
+ @repo.create_branch("foo_branch", "c004")
+ }
+ end
+
+ test "should check the target is existing when creating" do
+ assert_raise(Rugged::OdbError){
+ @repo.create_branch("foo_branch", "8496071c1b46c854b31185ea97743be6a877447a")
+ }
+ end
+
+ # 0c37a5391bbff43c37f0d0371823a5509eed5b1d tag
+ # c47800c7266a2be04c571c04d5a6614691ea99bd commit
+ # a8233120f6ad708f843d861ce2b7228ec4e3dec6 blob
+ test "should target is a tag or commit before creating a branch" do
+ assert_raise(TypeError){
+ @repo.create_branch("foo_branch", "a8233120f6ad708f843d861ce2b7228ec4e3dec6")
+ }
+ end
+
+ test "forbids to re-create an already existing branch" do
+ @repo.create_branch("foo_branch", "0c37a5391bbff43c37f0d0371823a5509eed5b1d")
+ assert_raise(Rugged::ReferenceError) {
+ @repo.create_branch("foo_branch", "0c37a5391bbff43c37f0d0371823a5509eed5b1d")
+ }
+ end
+
+ test "allows to force re-creation an already existing branch" do
+ @repo.create_branch("foo_branch", "0c37a5391bbff43c37f0d0371823a5509eed5b1d")
+ oid = @repo.create_branch("foo_branch", "0c37a5391bbff43c37f0d0371823a5509eed5b1d",true)
+ assert_equal oid, "0c37a5391bbff43c37f0d0371823a5509eed5b1d"
+ end
+
+ test "can rename a branch" do
+ ref = Rugged::Reference.lookup(@repo, "refs/heads/master")
+ @repo.create_branch("foo", ref.target )
+ @repo.move_branch("foo", "foo_branch")
+ branches = @repo.refs(/foo_branch/)
+ assert_equal branches.size, 1
+ assert_equal branches[0].target, ref.target
+ assert_empty @repo.refs(/foo$/)
+ end
+
+ test "should detect an invalid 'old name' branch when renaming/moving" do
+ assert_raise(TypeError){@repo.move_branch(1,"foo_branch")}
+ end
+
+ test "should detect an invalid 'new name' branch when renaming/moving" do
+ assert_raise(TypeError){@repo.move_branch("foo",1)}
+ end
+
+ test "should detect an inexisting 'old name' branch when renaming/moving" do
+ assert_raise(Rugged::ReferenceError){@repo.move_branch("fii", "foo_branch")}
+ end
+
+ test "forbids overwriting existing branch is not forced to" do
+ ref = Rugged::Reference.lookup(@repo, "refs/heads/master")
+ @repo.create_branch("foo", ref.target )
+ @repo.create_branch("foo_branch", ref.target )
+ assert_raise(Rugged::ReferenceError){@repo.move_branch("foo", "foo_branch")}
+ end
+
+ test "forbids overwriting existing branch when forced to" do
+ ref = Rugged::Reference.lookup(@repo, "refs/heads/master")
+ @repo.create_branch("foo", ref.target )
+ @repo.create_branch("foo_branch", ref.target )
+ @repo.move_branch("foo", "foo_branch", true)
+ branches = @repo.refs(/foo_branch/)
+ assert_equal branches.size, 1
+ assert_equal branches[0].target, ref.target
+ assert_empty @repo.refs(/foo$/)
+ end
+
+ test "can delete a branch (default type)" do
+ ref = Rugged::Reference.lookup(@repo, "refs/heads/master")
+ @repo.create_branch("foo", ref.target )
+ @repo.delete_branch("foo")
+ branches = @repo.refs(/foo$/)
+ assert_empty branches
+ end
+
+ test "should check the name of the branch is of right type before deleting" do
+ assert_raise(TypeError){@repo.delete_branch(1)}
+ end
+
+ test "should check the type of the branch before deleting" do
+ assert_raise(TypeError){@repo.delete_branch("foo", :foo)}
+ end
+
+ test "can delete a local branch" do
+ ref = Rugged::Reference.lookup(@repo, "refs/heads/master")
+ @repo.create_branch("foo", ref.target )
+ @repo.delete_branch("foo", :local)
+ branches = @repo.refs(/foo$/)
+ assert_empty branches
+ end
+
+ test "can iterate on branch names" do
+ ref = Rugged::Reference.lookup(@repo, "refs/heads/master")
+ @repo.create_branch("foo", ref.target )
+ @repo.create_branch("foo_branch", ref.target )
+ branch_names = []
+ @repo.each_branch do |name|
+ branch_names << name
+ end
+ assert_equal branch_names.size, 4
+# assert branch_names.include?("master"), "Branch list does not include 'master'"
+# assert branch_names.include?("packed"), "Branch list does not include 'packed'"
+# assert branch_names.include?("foo"), "Branch list does not include 'foo'"
+# assert branch_names.include?("foo_branch"), "Branch list does not include 'foo_branch'"

This comment has been minimized.

Show comment Hide comment
@sleeper

sleeper Jun 20, 2012

Note that this one are commented as they were failing due to an inconsistency in libgit2. I provided a fix that have been included in libgit2, so as soon as the "local" delivery of libgit2 is renewed, we should be able to remove the comments :)

@sleeper

sleeper Jun 20, 2012

Note that this one are commented as they were failing due to an inconsistency in libgit2. I provided a fix that have been included in libgit2, so as soon as the "local" delivery of libgit2 is renewed, we should be able to remove the comments :)

+ end
+
+end