Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions lib/git/base/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<Git::Object::Commit>
def merge_base(*args)
shas = self.lib.merge_base(*args)
shas.map { |sha| gcommit(sha) }
end

end

end
Expand Down
15 changes: 15 additions & 0 deletions lib/git/lib.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
144 changes: 144 additions & 0 deletions tests/units/test_merge_base.rb
Original file line number Diff line number Diff line change
@@ -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