diff --git a/README.md b/README.md index 286e355e..bc82b9e0 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,8 @@ And here are the operations that will need to write to your git repository. g.merge(g.branch('master')) g.merge([branch1, branch2]) + g.merge_base('branch1', 'branch2') + r = g.add_remote(name, uri) # Git::Remote r = g.add_remote(name, Git::Base) # Git::Remote diff --git a/lib/git/base/factory.rb b/lib/git/base/factory.rb index b97bfab5..e0cada61 100644 --- a/lib/git/base/factory.rb +++ b/lib/git/base/factory.rb @@ -68,6 +68,14 @@ def tag(tag_name) Git::Object.new(self, tag_name, 'tag', true) end + # Find as good common ancestors as possible for a merge + # example: g.merge_base('master', 'some_branch', 'some_sha', octopus: true) + # returns Array + def merge_base(*args) + shas = self.lib.merge_base(*args) + shas.map { |sha| gcommit(sha) } + end + end end diff --git a/lib/git/diff.rb b/lib/git/diff.rb index ff819be0..88f47d0b 100644 --- a/lib/git/diff.rb +++ b/lib/git/diff.rb @@ -130,7 +130,7 @@ def process_full_diff if @full_diff.encoding.name != "UTF-8" full_diff_utf8_encoded = @full_diff.encode("UTF-8", "binary", { :invalid => :replace, :undef => :replace }) else - full_diff_utf8_encoded = @full_diff + full_diff_utf8_encoded = @full_diff.encode("UTF-8", "UTF-8", { :invalid => :replace, :undef => :replace }) end full_diff_utf8_encoded.split("\n").each do |line| if m = /^diff --git a\/(.*?) b\/(.*?)/.match(line) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index fc390af5..a698cf3e 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -666,6 +666,21 @@ def merge(branch, message = nil) command('merge', arr_opts) end + def merge_base(*args) + opts = args.last.is_a?(Hash) ? args.pop : {} + + arg_opts = [] + + arg_opts << '--octopus' if opts[:octopus] + arg_opts << '--independent' if opts[:independent] + arg_opts << '--fork-point' if opts[:fork_point] + arg_opts << '--all' if opts[:all] + + arg_opts += args + + command('merge-base', arg_opts).lines.map(&:strip) + end + def unmerged unmerged = [] command_lines('diff', ["--cached"]).each do |line| @@ -747,6 +762,7 @@ def fetch(remote, opts) arr_opts << opts[:ref] if opts[:ref] arr_opts << '--tags' if opts[:t] || opts[:tags] arr_opts << '--prune' if opts[:p] || opts[:prune] + arr_opts << '--unshallow' if opts[:unshallow] command('fetch', arr_opts) end diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index ff5446f1..25e42022 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -13,7 +13,16 @@ def setup set_file_paths @lib = Git.open(@wdir).lib end - + + def test_fetch_unshallow + in_temp_dir do |dir| + git = Git.clone("file://#{@wdir}", "shallow", path: dir, depth: 1).lib + assert_equal(1, git.log_commits.length) + git.fetch("file://#{@wdir}", unshallow: true) + assert_equal(71, git.log_commits.length) + end + end + def test_commit_data data = @lib.commit_data('1cc8667014381') assert_equal('scott Chacon 1194561188 -0800', data['author']) diff --git a/tests/units/test_merge_base.rb b/tests/units/test_merge_base.rb new file mode 100755 index 00000000..8d6b09d5 --- /dev/null +++ b/tests/units/test_merge_base.rb @@ -0,0 +1,144 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' + +class TestMergeBase < Test::Unit::TestCase + def setup + set_file_paths + end + + def test_branch_and_master_merge_base + in_temp_dir do |_path| + repo = Git.clone(@wbare, 'branch_merge_test') + Dir.chdir('branch_merge_test') do + true_ancestor_sha = repo.gcommit('master').sha + + add_commit(repo, 'new_branch') + add_commit(repo, 'master') + + ancestors = repo.merge_base('master', 'new_branch') + assert_equal(ancestors.size, 1) # there is only one true ancestor + assert_equal(ancestors.first.sha, true_ancestor_sha) # proper common ancestor + end + end + end + + def test_branch_and_master_independent_merge_base + in_temp_dir do |_path| + repo = Git.clone(@wbare, 'branch_merge_test') + Dir.chdir('branch_merge_test') do + true_ancestor_sha = repo.gcommit('master').sha + + add_commit(repo, 'new_branch') + add_commit(repo, 'master') + + independent_commits = repo.merge_base(true_ancestor_sha, 'master', 'new_branch', independent: true) + assert_equal(independent_commits.size, 2) # both new master and a branch are unreachable from each other + true_independent_commits_shas = [repo.gcommit('master').sha, repo.gcommit('new_branch').sha] + assert_equal(independent_commits.map(&:sha).sort, true_independent_commits_shas.sort) + end + end + end + + def test_branch_and_master_fork_point_merge_base + in_temp_dir do |_path| + repo = Git.clone(@wbare, 'branch_merge_test') + Dir.chdir('branch_merge_test') do + add_commit(repo, 'master') + + true_ancestor_sha = repo.gcommit('master').sha + + add_commit(repo, 'new_branch') + + repo.reset_hard(repo.gcommit('HEAD^')) + + add_commit(repo, 'master') + + ancestors = repo.merge_base('master', 'new_branch', fork_point: true) + assert_equal(ancestors.size, 1) # there is only one true ancestor + assert_equal(ancestors.first.sha, true_ancestor_sha) # proper common ancestor + end + end + end + + def test_branch_and_master_all_merge_base + in_temp_dir do |_path| + repo = Git.clone(@wbare, 'branch_merge_test') + Dir.chdir('branch_merge_test') do + add_commit(repo, 'new_branch_1') + + first_commit_sha = repo.gcommit('new_branch_1').sha + + add_commit(repo, 'new_branch_2') + + second_commit_sha = repo.gcommit('new_branch_2').sha + + repo.branch('new_branch_1').merge('new_branch_2') + repo.branch('new_branch_2').merge('new_branch_1^') + + add_commit(repo, 'new_branch_1') + add_commit(repo, 'new_branch_2') + + true_ancestors_shas = [first_commit_sha, second_commit_sha] + + ancestors = repo.merge_base('new_branch_1', 'new_branch_2') + assert_equal(ancestors.size, 1) # default behavior returns only one ancestor + assert(true_ancestors_shas.include?(ancestors.first.sha)) + + all_ancestors = repo.merge_base('new_branch_1', 'new_branch_2', all: true) + assert_equal(all_ancestors.size, 2) # there are two best ancestors in such case + assert_equal(all_ancestors.map(&:sha).sort, true_ancestors_shas.sort) + end + end + end + + def test_branches_and_master_merge_base + in_temp_dir do |_path| + repo = Git.clone(@wbare, 'branch_merge_test') + Dir.chdir('branch_merge_test') do + add_commit(repo, 'new_branch_1') + add_commit(repo, 'master') + + non_octopus_ancestor_sha = repo.gcommit('master').sha + + add_commit(repo, 'new_branch_2') + add_commit(repo, 'master') + + ancestors = repo.merge_base('master', 'new_branch_1', 'new_branch_2') + assert_equal(ancestors.size, 1) # there is only one true ancestor + assert_equal(ancestors.first.sha, non_octopus_ancestor_sha) # proper common ancestor + end + end + end + + def test_branches_and_master_octopus_merge_base + in_temp_dir do |_path| + repo = Git.clone(@wbare, 'branch_merge_test') + Dir.chdir('branch_merge_test') do + true_ancestor_sha = repo.gcommit('master').sha + + add_commit(repo, 'new_branch_1') + add_commit(repo, 'master') + add_commit(repo, 'new_branch_2') + add_commit(repo, 'master') + + ancestors = repo.merge_base('master', 'new_branch_1', 'new_branch_2', octopus: true) + assert_equal(ancestors.size, 1) # there is only one true ancestor + assert_equal(ancestors.first.sha, true_ancestor_sha) # proper common ancestor + end + end + end + + private + + def add_commit(repo, branch_name) + @commit_number ||= 0 + @commit_number += 1 + + repo.branch(branch_name).in_branch("test commit #{@commit_number}") do + new_file("new_file_#{@commit_number}", 'hello') + repo.add + true + end + end +end