Skip to content

Commit

Permalink
Add Git::URL parse and clone_to methods
Browse files Browse the repository at this point in the history
Signed-off-by: James Couball <jcouball@yahoo.com>
  • Loading branch information
jcouball committed Apr 21, 2022
1 parent 0a43d8b commit b809aaf
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
1 change: 1 addition & 0 deletions git.gemspec
Expand Up @@ -26,6 +26,7 @@ Gem::Specification.new do |s|
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=)
s.requirements = ['git 1.6.0.0, or greater']

s.add_runtime_dependency 'addressable', '~> 2.8'
s.add_runtime_dependency 'rchardet', '~> 1.8'

s.add_development_dependency 'bump', '~> 0.10'
Expand Down
1 change: 1 addition & 0 deletions lib/git.rb
Expand Up @@ -21,6 +21,7 @@
require 'git/status'
require 'git/stash'
require 'git/stashes'
require 'git/url'
require 'git/version'
require 'git/working_directory'
require 'git/worktree'
Expand Down
108 changes: 108 additions & 0 deletions lib/git/url.rb
@@ -0,0 +1,108 @@
# frozen_string_literal: true

require 'addressable/uri'

module Git
# Methods for parsing a Git URL
#
# Any URL that can be passed to `git clone` can be parsed by this class.
#
# @see https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a GIT URLs
# @see https://github.com/sporkmonger/addressable Addresable::URI
#
# @api public
#
class URL
GIT_ALTERNATIVE_SSH_SYNTAX = %r{
^
(?:(?<user>[^@/]+)@)? # user or nil
(?<host>[^:/]+) # host is required
:(?!/) # : serparator is required, but must not be followed by /
(?<path>.*?) # path is required
$
}x.freeze

# Parse a Git URL and return an Addressable::URI object
#
# The URI returned can be converted back to a string with 'to_s'. This is
# guaranteed to return the same URL string that was parsed.
#
# @example
# uri = Git::URL.parse('https://github.com/ruby-git/ruby-git.git')
# #=> #<Addressable::URI:0x44c URI:https://github.com/ruby-git/ruby-git.git>
# uri.scheme #=> "https"
# uri.host #=> "github.com"
# uri.path #=> "/ruby-git/ruby-git.git"
#
# Git::URL.parse('/Users/James/projects/ruby-git')
# #=> #<Addressable::URI:0x438 URI:/Users/James/projects/ruby-git>
#
# @param url [String] the Git URL to parse
#
# @return [Addressable::URI] the parsed URI
#
def self.parse(url)
if !url.start_with?('file:') && (m = GIT_ALTERNATIVE_SSH_SYNTAX.match(url))
GitAltURI.new(user: m[:user], host: m[:host], path: m[:path])
else
Addressable::URI.parse(url)
end
end

# The name `git clone` would use for the repository directory for the given URL
#
# @example
# Git::URL.clone_to('https://github.com/ruby-git/ruby-git.git') #=> 'ruby-git'
#
# @param url [String] the Git URL containing the repository directory
#
# @return [String] the name of the repository directory
#
def self.clone_to(url)
uri = parse(url)
path_parts = uri.path.split('/')
path_parts.pop if path_parts.last == '.git'

path_parts.last.sub(/\.git$/, '')
end
end

# The URI for git's alternative scp-like syntax
#
# This class is necessary to ensure that #to_s returns the same string
# that was passed to the initializer.
#
# @api public
#
class GitAltURI < Addressable::URI
# Create a new GitAltURI object
#
# @example
# uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
# uri.to_s #=> 'james@github.com/james/ruby-git'
#
# @param user [String, nil] the user from the URL or nil
# @param host [String] the host from the URL
# @param path [String] the path from the URL
#
def initialize(user:, host:, path:)
super(scheme: 'git-alt', user: user, host: host, path: path)
end

# Convert the URI to a String
#
# @example
# uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
# uri.to_s #=> 'james@github.com/james/ruby-git'
#
# @return [String] the URI as a String
#
def to_s
if user
"#{user}@#{host}:#{path[1..-1]}"
else
"#{host}:#{path[1..-1]}"
end
end
end
end
137 changes: 137 additions & 0 deletions tests/units/test_url.rb
@@ -0,0 +1,137 @@
require 'test/unit'

GIT_URLS = [
{
url: 'ssh://host.xz/path/to/repo.git/',
expected_attributes: { scheme: 'ssh', host: 'host.xz', path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'ssh://host.xz:4443/path/to/repo.git/',
expected_attributes: { scheme: 'ssh', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'ssh:///path/to/repo.git/',
expected_attributes: { scheme: 'ssh', host: '', path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'user@host.xz:path/to/repo.git/',
expected_attributes: { scheme: 'git-alt', user: 'user', host: 'host.xz', path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'host.xz:path/to/repo.git/',
expected_attributes: { scheme: 'git-alt', host: 'host.xz', path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'git://host.xz:4443/path/to/repo.git/',
expected_attributes: { scheme: 'git', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'git://user@host.xz:4443/path/to/repo.git/',
expected_attributes: { scheme: 'git', user: 'user', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'https://host.xz/path/to/repo.git/',
expected_attributes: { scheme: 'https', host: 'host.xz', path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'https://host.xz:4443/path/to/repo.git/',
expected_attributes: { scheme: 'https', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'ftps://host.xz:4443/path/to/repo.git/',
expected_attributes: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'ftps://host.xz:4443/path/to/repo.git/',
expected_attributes: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'file:./relative-path/to/repo.git/',
expected_attributes: { scheme: 'file', path: './relative-path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'file:///path/to/repo.git/',
expected_attributes: { scheme: 'file', host: '', path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: 'file:///path/to/repo.git',
expected_attributes: { scheme: 'file', host: '', path: '/path/to/repo.git' },
expected_clone_to: 'repo'
},
{
url: 'file://host.xz/path/to/repo.git',
expected_attributes: { scheme: 'file', host: 'host.xz', path: '/path/to/repo.git' },
expected_clone_to: 'repo'
},
{
url: '/path/to/repo.git/',
expected_attributes: { path: '/path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: '/path/to/bare-repo/.git',
expected_attributes: { path: '/path/to/bare-repo/.git' },
expected_clone_to: 'bare-repo'
},
{
url: 'relative-path/to/repo.git/',
expected_attributes: { path: 'relative-path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: './relative-path/to/repo.git/',
expected_attributes: { path: './relative-path/to/repo.git/' },
expected_clone_to: 'repo'
},
{
url: '../ruby-git/.git',
expected_attributes: { path: '../ruby-git/.git' },
expected_clone_to: 'ruby-git'
}
].freeze

# Tests for the Git::URL class
#
class TestURL < Test::Unit::TestCase
def test_parse
GIT_URLS.each do |url_data|
url = url_data[:url]
expected_attributes = url_data[:expected_attributes]
actual_attributes = Git::URL.parse(url).to_hash.delete_if {| key, value | value.nil? }
assert_equal(expected_attributes, actual_attributes, "Failed to parse URL '#{url}' correctly")
end
end

def test_clone_to
GIT_URLS.each do |url_data|
url = url_data[:url]
expected_clone_to = url_data[:expected_clone_to]
actual_repo_name = Git::URL.clone_to(url)
assert_equal(
expected_clone_to, actual_repo_name,
"Failed to determine the repository directory for URL '#{url}' correctly"
)
end
end

def test_to_s
GIT_URLS.each do |url_data|
url = url_data[:url]
to_s = Git::URL.parse(url).to_s
assert_equal(url, to_s, "Parsed URI#to_s does not return the original URL '#{url}' correctly")
end
end
end

0 comments on commit b809aaf

Please sign in to comment.