Skip to content

Commit a4fbb6b

Browse files
Evgenii Pecherkintarcinil
Evgenii Pecherkin
authored andcommitted
Support merge-base (#370)
Signed-off-by: Evgenii Pecherkin <evgenii@toptal.com>
1 parent b7a1a67 commit a4fbb6b

File tree

4 files changed

+169
-0
lines changed

4 files changed

+169
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ And here are the operations that will need to write to your git repository.
216216
g.merge(g.branch('master'))
217217
g.merge([branch1, branch2])
218218

219+
g.merge_base('branch1', 'branch2')
220+
219221
r = g.add_remote(name, uri) # Git::Remote
220222
r = g.add_remote(name, Git::Base) # Git::Remote
221223

lib/git/base/factory.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ def tag(tag_name)
6868
Git::Object.new(self, tag_name, 'tag', true)
6969
end
7070

71+
# Find as good common ancestors as possible for a merge
72+
# example: g.merge_base('master', 'some_branch', 'some_sha', octopus: true)
73+
# returns Array<Git::Object::Commit>
74+
def merge_base(*args)
75+
shas = self.lib.merge_base(*args)
76+
shas.map { |sha| gcommit(sha) }
77+
end
78+
7179
end
7280

7381
end

lib/git/lib.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,21 @@ def merge(branch, message = nil)
666666
command('merge', arr_opts)
667667
end
668668

669+
def merge_base(*args)
670+
opts = args.last.is_a?(Hash) ? args.pop : {}
671+
672+
arg_opts = []
673+
674+
arg_opts << '--octopus' if opts[:octopus]
675+
arg_opts << '--independent' if opts[:independent]
676+
arg_opts << '--fork-point' if opts[:fork_point]
677+
arg_opts << '--all' if opts[:all]
678+
679+
arg_opts += args
680+
681+
command('merge-base', arg_opts).lines.map(&:strip)
682+
end
683+
669684
def unmerged
670685
unmerged = []
671686
command_lines('diff', ["--cached"]).each do |line|

tests/units/test_merge_base.rb

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env ruby
2+
3+
require File.dirname(__FILE__) + '/../test_helper'
4+
5+
class TestMergeBase < Test::Unit::TestCase
6+
def setup
7+
set_file_paths
8+
end
9+
10+
def test_branch_and_master_merge_base
11+
in_temp_dir do |_path|
12+
repo = Git.clone(@wbare, 'branch_merge_test')
13+
Dir.chdir('branch_merge_test') do
14+
true_ancestor_sha = repo.gcommit('master').sha
15+
16+
add_commit(repo, 'new_branch')
17+
add_commit(repo, 'master')
18+
19+
ancestors = repo.merge_base('master', 'new_branch')
20+
assert_equal(ancestors.size, 1) # there is only one true ancestor
21+
assert_equal(ancestors.first.sha, true_ancestor_sha) # proper common ancestor
22+
end
23+
end
24+
end
25+
26+
def test_branch_and_master_independent_merge_base
27+
in_temp_dir do |_path|
28+
repo = Git.clone(@wbare, 'branch_merge_test')
29+
Dir.chdir('branch_merge_test') do
30+
true_ancestor_sha = repo.gcommit('master').sha
31+
32+
add_commit(repo, 'new_branch')
33+
add_commit(repo, 'master')
34+
35+
independent_commits = repo.merge_base(true_ancestor_sha, 'master', 'new_branch', independent: true)
36+
assert_equal(independent_commits.size, 2) # both new master and a branch are unreachable from each other
37+
true_independent_commits_shas = [repo.gcommit('master').sha, repo.gcommit('new_branch').sha]
38+
assert_equal(independent_commits.map(&:sha).sort, true_independent_commits_shas.sort)
39+
end
40+
end
41+
end
42+
43+
def test_branch_and_master_fork_point_merge_base
44+
in_temp_dir do |_path|
45+
repo = Git.clone(@wbare, 'branch_merge_test')
46+
Dir.chdir('branch_merge_test') do
47+
add_commit(repo, 'master')
48+
49+
true_ancestor_sha = repo.gcommit('master').sha
50+
51+
add_commit(repo, 'new_branch')
52+
53+
repo.reset_hard(repo.gcommit('HEAD^'))
54+
55+
add_commit(repo, 'master')
56+
57+
ancestors = repo.merge_base('master', 'new_branch', fork_point: true)
58+
assert_equal(ancestors.size, 1) # there is only one true ancestor
59+
assert_equal(ancestors.first.sha, true_ancestor_sha) # proper common ancestor
60+
end
61+
end
62+
end
63+
64+
def test_branch_and_master_all_merge_base
65+
in_temp_dir do |_path|
66+
repo = Git.clone(@wbare, 'branch_merge_test')
67+
Dir.chdir('branch_merge_test') do
68+
add_commit(repo, 'new_branch_1')
69+
70+
first_commit_sha = repo.gcommit('new_branch_1').sha
71+
72+
add_commit(repo, 'new_branch_2')
73+
74+
second_commit_sha = repo.gcommit('new_branch_2').sha
75+
76+
repo.branch('new_branch_1').merge('new_branch_2')
77+
repo.branch('new_branch_2').merge('new_branch_1^')
78+
79+
add_commit(repo, 'new_branch_1')
80+
add_commit(repo, 'new_branch_2')
81+
82+
true_ancestors_shas = [first_commit_sha, second_commit_sha]
83+
84+
ancestors = repo.merge_base('new_branch_1', 'new_branch_2')
85+
assert_equal(ancestors.size, 1) # default behavior returns only one ancestor
86+
assert(true_ancestors_shas.include?(ancestors.first.sha))
87+
88+
all_ancestors = repo.merge_base('new_branch_1', 'new_branch_2', all: true)
89+
assert_equal(all_ancestors.size, 2) # there are two best ancestors in such case
90+
assert_equal(all_ancestors.map(&:sha).sort, true_ancestors_shas.sort)
91+
end
92+
end
93+
end
94+
95+
def test_branches_and_master_merge_base
96+
in_temp_dir do |_path|
97+
repo = Git.clone(@wbare, 'branch_merge_test')
98+
Dir.chdir('branch_merge_test') do
99+
add_commit(repo, 'new_branch_1')
100+
add_commit(repo, 'master')
101+
102+
non_octopus_ancestor_sha = repo.gcommit('master').sha
103+
104+
add_commit(repo, 'new_branch_2')
105+
add_commit(repo, 'master')
106+
107+
ancestors = repo.merge_base('master', 'new_branch_1', 'new_branch_2')
108+
assert_equal(ancestors.size, 1) # there is only one true ancestor
109+
assert_equal(ancestors.first.sha, non_octopus_ancestor_sha) # proper common ancestor
110+
end
111+
end
112+
end
113+
114+
def test_branches_and_master_octopus_merge_base
115+
in_temp_dir do |_path|
116+
repo = Git.clone(@wbare, 'branch_merge_test')
117+
Dir.chdir('branch_merge_test') do
118+
true_ancestor_sha = repo.gcommit('master').sha
119+
120+
add_commit(repo, 'new_branch_1')
121+
add_commit(repo, 'master')
122+
add_commit(repo, 'new_branch_2')
123+
add_commit(repo, 'master')
124+
125+
ancestors = repo.merge_base('master', 'new_branch_1', 'new_branch_2', octopus: true)
126+
assert_equal(ancestors.size, 1) # there is only one true ancestor
127+
assert_equal(ancestors.first.sha, true_ancestor_sha) # proper common ancestor
128+
end
129+
end
130+
end
131+
132+
private
133+
134+
def add_commit(repo, branch_name)
135+
@commit_number ||= 0
136+
@commit_number += 1
137+
138+
repo.branch(branch_name).in_branch("test commit #{@commit_number}") do
139+
new_file("new_file_#{@commit_number}", 'hello')
140+
repo.add
141+
true
142+
end
143+
end
144+
end

0 commit comments

Comments
 (0)