Skip to content

Get default branch of remote repository (Git.default_branch) #571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 4, 2023
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 @@ -197,6 +197,8 @@ g.show('v2.8', 'README.md')
Git.ls_remote('https://github.com/ruby-git/ruby-git.git') # returns a hash containing the available references of the repo.
Git.ls_remote('/path/to/local/repo')
Git.ls_remote() # same as Git.ls_remote('.')

Git.default_branch('https://github.com/ruby-git/ruby-git') #=> 'master'
```

And here are the operations that will need to write to your git repository.
Expand Down
40 changes: 40 additions & 0 deletions lib/git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,46 @@ def self.clone(repository_url, directory = nil, options = {})
Base.clone(repository_url, directory, options)
end

# Returns the name of the default branch of the given repository
#
# @example with a URI string
# Git.default_branch('https://github.com/ruby-git/ruby-git') # => 'master'
# Git.default_branch('https://github.com/rspec/rspec-core') # => 'main'
#
# @example with a URI object
# repository_uri = URI('https://github.com/ruby-git/ruby-git')
# Git.default_branch(repository_uri) # => 'master'
#
# @example with a local repository
# Git.default_branch('.') # => 'master'
#
# @example with a local repository Pathname
# repository_path = Pathname('.')
# Git.default_branch(repository_path) # => 'master'
#
# @example with the logging option
# logger = Logger.new(STDOUT, level: Logger::INFO)
# Git.default_branch('.', log: logger) # => 'master'
# I, [2022-04-13T16:01:33.221596 #18415] INFO -- : git '-c' 'core.quotePath=true' '-c' 'color.ui=false' ls-remote '--symref' '--' '.' 'HEAD' 2>&1
#
# @param repository [URI, Pathname, String] The (possibly remote) repository to get the default branch name for
#
# See [GIT URLS](https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a)
# for more information.
#
# @param [Hash] options The options for this command (see list of valid
# options below)
#
# @option options [Logger] :log A logger to use for Git operations. Git
# commands are logged at the `:info` level. Additional logging is done
# at the `:debug` level.
#
# @return [String] the name of the default branch
#
def self.default_branch(repository, options = {})
Base.repository_default_branch(repository, options)
end

# Export the current HEAD (or a branch, if <tt>options[:branch]</tt>
# is specified) into the +name+ directory, then remove all traces of git from the
# directory.
Expand Down
5 changes: 5 additions & 0 deletions lib/git/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ def self.clone(repository_url, directory, options = {})
new(new_options)
end

# (see Git.default_branch)
def self.repository_default_branch(repository, options = {})
Git::Lib.new(nil, options[:log]).repository_default_branch(repository)
end

# Returns (and initialize if needed) a Git::Config instance
#
# @return [Git::Config] the current config instance.
Expand Down
18 changes: 18 additions & 0 deletions lib/git/lib.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,24 @@ def return_base_opts_from_clone(clone_dir, opts)
base_opts
end

# Returns the name of the default branch of the given repository
#
# @param repository [URI, Pathname, String] The (possibly remote) repository to clone from
#
# @return [String] the name of the default branch
#
def repository_default_branch(repository)
output = command('ls-remote', '--symref', '--', repository, 'HEAD')

match_data = output.match(%r{^ref: refs/remotes/origin/(?<default_branch>[^\t]+)\trefs/remotes/origin/HEAD$})
return match_data[:default_branch] if match_data

match_data = output.match(%r{^ref: refs/heads/(?<default_branch>[^\t]+)\tHEAD$})
return match_data[:default_branch] if match_data

raise 'Unable to determine the default branch'
end

## READ COMMANDS ##

#
Expand Down
41 changes: 41 additions & 0 deletions tests/units/test_git_default_branch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env ruby

require File.dirname(__FILE__) + '/../test_helper'

require 'logger'
require 'stringio'

# Tests for Git::Lib#repository_default_branch
#
class TestLibRepositoryDefaultBranch < Test::Unit::TestCase
def test_default_branch
repository = 'new_repo'
in_temp_dir do
create_local_repository(repository, initial_branch: 'main')
assert_equal('main', Git.default_branch(repository))
end
end

def test_default_branch_with_logging
repository = 'new_repo'
in_temp_dir do
create_local_repository(repository, initial_branch: 'main')
log_device = StringIO.new
logger = Logger.new(log_device, level: Logger::INFO)
Git.default_branch(repository, log: logger)
assert_match(/git.*ls-remote/, log_device.string)
end
end

private

def create_local_repository(subdirectory, initial_branch: 'main')
git = Git.init(subdirectory, initial_branch: initial_branch)

FileUtils.cd(subdirectory) do
File.write('README.md', '# This is a README')
git.add('README.md')
git.commit('Initial commit')
end
end
end
96 changes: 96 additions & 0 deletions tests/units/test_lib_repository_default_branch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env ruby

require File.dirname(__FILE__) + '/../test_helper'

# Tests for Git::Lib#repository_default_branch
#
class TestLibRepositoryDefaultBranch < Test::Unit::TestCase
def setup
clone_working_repo
@git = Git.open(@wdir)

@lib = Git.open(@wdir).lib
end

# This is the one real test that actually calls git. The rest of the tests
# mock Git::Lib#command to return specific responses.
#
def test_local_repository
in_temp_dir do
git = Git.init('new_repo', initial_branch: 'main')
FileUtils.cd('new_repo') do
File.write('README.md', '# This is a README')
git.add('README.md')
git.commit('Initial commit')
end
FileUtils.touch('new_repo/README.md')

assert_equal('main', @lib.repository_default_branch('new_repo'))
end
end

def mock_command(lib, repository, response)
test_case = self
lib.define_singleton_method(:command) do |cmd, *opts, &_block|
test_case.assert_equal('ls-remote', cmd)
test_case.assert_equal(['--symref', '--', repository, 'HEAD'], opts.flatten)
response
end
end

def test_remote_repository
repository = 'https://github.com/ruby-git/ruby-git'
mock_command(@lib, repository, <<~RESPONSE)
ref: refs/heads/default_branch\tHEAD
292087efabc8423c3cf616d78fac5311d58e7425\tHEAD
RESPONSE
assert_equal('default_branch', @lib.repository_default_branch(repository))
end

def test_local_repository_with_origin
repository = 'https://github.com/ruby-git/ruby-git'
mock_command(@lib, repository, <<~RESPONSE)
ref: refs/heads/master\tHEAD
292087efabc8423c3cf616d78fac5311d58e7425\tHEAD
ref: refs/remotes/origin/default_branch\trefs/remotes/origin/HEAD
292087efabc8423c3cf616d78fac5311d58e7425\trefs/remotes/origin/HEAD
RESPONSE
assert_equal('default_branch', @lib.repository_default_branch(repository))
end

def test_local_repository_without_remotes
repository = '.'
mock_command(@lib, repository, <<~RESPONSE)
ref: refs/heads/default_branch\tHEAD
d7b79c31113c42c7aa3fe915186c1d6bcd3fbd39\tHEAD
RESPONSE
assert_equal('default_branch', @lib.repository_default_branch(repository))
end

def test_repository_with_no_commits
# Local or remote, the result is the same
repository = '.'
mock_command(@lib, repository, '')
assert_raise_with_message(RuntimeError, 'Unable to determine the default branch') do
@lib.repository_default_branch(repository)
end
end

def test_repository_not_found
# Local or remote, the result is the same
repository = 'does_not_exist'
assert_raise(Git::FailedError) do
@lib.repository_default_branch(repository)
end
end

def test_not_a_repository
in_temp_dir do
repository = 'exists_but_not_a_repository'
FileUtils.mkdir repository
assert_raise(Git::FailedError) do
@lib.repository_default_branch(repository)
end
end
end
end