Skip to content

Commit acbfa48

Browse files
committed
Implememt Git.default_branch
Signed-off-by: James Couball <jcouball@yahoo.com>
1 parent 734e085 commit acbfa48

File tree

6 files changed

+200
-0
lines changed

6 files changed

+200
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ g.show('v2.8', 'README.md')
197197
Git.ls_remote('https://github.com/ruby-git/ruby-git.git') # returns a hash containing the available references of the repo.
198198
Git.ls_remote('/path/to/local/repo')
199199
Git.ls_remote() # same as Git.ls_remote('.')
200+
201+
Git.default_branch('https://github.com/ruby-git/ruby-git') #=> 'master'
200202
```
201203

202204
And here are the operations that will need to write to your git repository.

lib/git.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,46 @@ def self.clone(repository_url, directory = nil, options = {})
175175
Base.clone(repository_url, directory, options)
176176
end
177177

178+
# Returns the name of the default branch of the given repository
179+
#
180+
# @example with a URI string
181+
# Git.default_branch('https://github.com/ruby-git/ruby-git') # => 'master'
182+
# Git.default_branch('https://github.com/rspec/rspec-core') # => 'main'
183+
#
184+
# @example with a URI object
185+
# repository_uri = URI('https://github.com/ruby-git/ruby-git')
186+
# Git.default_branch(repository_uri) # => 'master'
187+
#
188+
# @example with a local repository
189+
# Git.default_branch('.') # => 'master'
190+
#
191+
# @example with a local repository Pathname
192+
# repository_path = Pathname('.')
193+
# Git.default_branch(repository_path) # => 'master'
194+
#
195+
# @example with the logging option
196+
# logger = Logger.new(STDOUT, level: Logger::INFO)
197+
# Git.default_branch('.', log: logger) # => 'master'
198+
# I, [2022-04-13T16:01:33.221596 #18415] INFO -- : git '-c' 'core.quotePath=true' '-c' 'color.ui=false' ls-remote '--symref' '--' '.' 'HEAD' 2>&1
199+
#
200+
# @param repository [URI, Pathname, String] The (possibly remote) repository to get the default branch name for
201+
#
202+
# See [GIT URLS](https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a)
203+
# for more information.
204+
#
205+
# @param [Hash] options The options for this command (see list of valid
206+
# options below)
207+
#
208+
# @option options [Logger] :log A logger to use for Git operations. Git
209+
# commands are logged at the `:info` level. Additional logging is done
210+
# at the `:debug` level.
211+
#
212+
# @return [String] the name of the default branch
213+
#
214+
def self.default_branch(repository, options = {})
215+
Base.repository_default_branch(repository, options)
216+
end
217+
178218
# Export the current HEAD (or a branch, if <tt>options[:branch]</tt>
179219
# is specified) into the +name+ directory, then remove all traces of git from the
180220
# directory.

lib/git/base.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def self.clone(repository_url, directory, options = {})
2424
new(new_options)
2525
end
2626

27+
# (see Git.default_branch)
28+
def self.repository_default_branch(repository, options = {})
29+
Git::Lib.new(nil, options[:log]).repository_default_branch(repository)
30+
end
31+
2732
# Returns (and initialize if needed) a Git::Config instance
2833
#
2934
# @return [Git::Config] the current config instance.

lib/git/lib.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,24 @@ def return_base_opts_from_clone(clone_dir, opts)
124124
base_opts
125125
end
126126

127+
# Returns the name of the default branch of the given repository
128+
#
129+
# @param repository [URI, Pathname, String] The (possibly remote) repository to clone from
130+
#
131+
# @return [String] the name of the default branch
132+
#
133+
def repository_default_branch(repository)
134+
output = command('ls-remote', ['--symref', '--', repository, 'HEAD'])
135+
136+
match_data = output.match(%r{^ref: refs/remotes/origin/(?<default_branch>[^\t]+)\trefs/remotes/origin/HEAD$})
137+
return match_data[:default_branch] if match_data
138+
139+
match_data = output.match(%r{^ref: refs/heads/(?<default_branch>[^\t]+)\tHEAD$})
140+
return match_data[:default_branch] if match_data
141+
142+
raise 'Unable to determine the default branch'
143+
end
144+
127145
## READ COMMANDS ##
128146

129147
#
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env ruby
2+
3+
require File.dirname(__FILE__) + '/../test_helper'
4+
5+
require 'logger'
6+
require 'stringio'
7+
8+
# Tests for Git::Lib#repository_default_branch
9+
#
10+
class TestLibRepositoryDefaultBranch < Test::Unit::TestCase
11+
def test_default_branch
12+
repository = 'new_repo'
13+
in_temp_dir do
14+
create_local_repository(repository, initial_branch: 'main')
15+
assert_equal('main', Git.default_branch(repository))
16+
end
17+
end
18+
19+
def test_default_branch_with_logging
20+
repository = 'new_repo'
21+
in_temp_dir do
22+
create_local_repository(repository, initial_branch: 'main')
23+
log_device = StringIO.new
24+
logger = Logger.new(log_device, level: Logger::INFO)
25+
Git.default_branch(repository, log: logger)
26+
assert_match(/git.*ls-remote/, log_device.string)
27+
end
28+
end
29+
30+
private
31+
32+
def create_local_repository(subdirectory, initial_branch: 'main')
33+
git = Git.init(subdirectory, initial_branch: initial_branch)
34+
35+
FileUtils.cd(subdirectory) do
36+
File.write('README.md', '# This is a README')
37+
git.add('README.md')
38+
git.commit('Initial commit')
39+
end
40+
end
41+
end
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env ruby
2+
3+
require File.dirname(__FILE__) + '/../test_helper'
4+
5+
# Tests for Git::Lib#repository_default_branch
6+
#
7+
class TestLibRepositoryDefaultBranch < Test::Unit::TestCase
8+
def setup
9+
set_file_paths
10+
@lib = Git.open(@wdir).lib
11+
end
12+
13+
# This is the one real test that actually calls git. The rest of the tests
14+
# mock Git::Lib#command to return specific responses.
15+
#
16+
def test_local_repository
17+
in_temp_dir do
18+
git = Git.init('new_repo', initial_branch: 'main')
19+
FileUtils.cd('new_repo') do
20+
File.write('README.md', '# This is a README')
21+
git.add('README.md')
22+
git.commit('Initial commit')
23+
end
24+
FileUtils.touch('new_repo/README.md')
25+
26+
assert_equal('main', @lib.repository_default_branch('new_repo'))
27+
end
28+
end
29+
30+
def mock_command(lib, repository, response)
31+
test_case = self
32+
lib.define_singleton_method(:command) do |cmd, *opts, &_block|
33+
test_case.assert_equal('ls-remote', cmd)
34+
test_case.assert_equal(['--symref', '--', repository, 'HEAD'], opts.flatten)
35+
response
36+
end
37+
end
38+
39+
def test_remote_repository
40+
repository = 'https://github.com/ruby-git/ruby-git'
41+
mock_command(@lib, repository, <<~RESPONSE)
42+
ref: refs/heads/default_branch\tHEAD
43+
292087efabc8423c3cf616d78fac5311d58e7425\tHEAD
44+
RESPONSE
45+
assert_equal('default_branch', @lib.repository_default_branch(repository))
46+
end
47+
48+
def test_local_repository_with_origin
49+
repository = 'https://github.com/ruby-git/ruby-git'
50+
mock_command(@lib, repository, <<~RESPONSE)
51+
ref: refs/heads/master\tHEAD
52+
292087efabc8423c3cf616d78fac5311d58e7425\tHEAD
53+
ref: refs/remotes/origin/default_branch\trefs/remotes/origin/HEAD
54+
292087efabc8423c3cf616d78fac5311d58e7425\trefs/remotes/origin/HEAD
55+
RESPONSE
56+
assert_equal('default_branch', @lib.repository_default_branch(repository))
57+
end
58+
59+
def test_local_repository_without_remotes
60+
repository = '.'
61+
mock_command(@lib, repository, <<~RESPONSE)
62+
ref: refs/heads/default_branch\tHEAD
63+
d7b79c31113c42c7aa3fe915186c1d6bcd3fbd39\tHEAD
64+
RESPONSE
65+
assert_equal('default_branch', @lib.repository_default_branch(repository))
66+
end
67+
68+
def test_repository_with_no_commits
69+
# Local or remote, the result is the same
70+
repository = '.'
71+
mock_command(@lib, repository, '')
72+
assert_raise_with_message(RuntimeError, 'Unable to determine the default branch') do
73+
@lib.repository_default_branch(repository)
74+
end
75+
end
76+
77+
def test_repository_not_found
78+
# Local or remote, the result is the same
79+
repository = 'does_not_exist'
80+
assert_raise(Git::GitExecuteError) do
81+
@lib.repository_default_branch(repository)
82+
end
83+
end
84+
85+
def test_not_a_repository
86+
in_temp_dir do
87+
repository = 'exists_but_not_a_repository'
88+
FileUtils.mkdir repository
89+
assert_raise(Git::GitExecuteError) do
90+
@lib.repository_default_branch(repository)
91+
end
92+
end
93+
end
94+
end

0 commit comments

Comments
 (0)