Skip to content

Commit b809aaf

Browse files
committed
Add Git::URL parse and clone_to methods
Signed-off-by: James Couball <jcouball@yahoo.com>
1 parent 0a43d8b commit b809aaf

File tree

4 files changed

+247
-0
lines changed

4 files changed

+247
-0
lines changed

git.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
2626
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=)
2727
s.requirements = ['git 1.6.0.0, or greater']
2828

29+
s.add_runtime_dependency 'addressable', '~> 2.8'
2930
s.add_runtime_dependency 'rchardet', '~> 1.8'
3031

3132
s.add_development_dependency 'bump', '~> 0.10'

lib/git.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
require 'git/status'
2222
require 'git/stash'
2323
require 'git/stashes'
24+
require 'git/url'
2425
require 'git/version'
2526
require 'git/working_directory'
2627
require 'git/worktree'

lib/git/url.rb

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# frozen_string_literal: true
2+
3+
require 'addressable/uri'
4+
5+
module Git
6+
# Methods for parsing a Git URL
7+
#
8+
# Any URL that can be passed to `git clone` can be parsed by this class.
9+
#
10+
# @see https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a GIT URLs
11+
# @see https://github.com/sporkmonger/addressable Addresable::URI
12+
#
13+
# @api public
14+
#
15+
class URL
16+
GIT_ALTERNATIVE_SSH_SYNTAX = %r{
17+
^
18+
(?:(?<user>[^@/]+)@)? # user or nil
19+
(?<host>[^:/]+) # host is required
20+
:(?!/) # : serparator is required, but must not be followed by /
21+
(?<path>.*?) # path is required
22+
$
23+
}x.freeze
24+
25+
# Parse a Git URL and return an Addressable::URI object
26+
#
27+
# The URI returned can be converted back to a string with 'to_s'. This is
28+
# guaranteed to return the same URL string that was parsed.
29+
#
30+
# @example
31+
# uri = Git::URL.parse('https://github.com/ruby-git/ruby-git.git')
32+
# #=> #<Addressable::URI:0x44c URI:https://github.com/ruby-git/ruby-git.git>
33+
# uri.scheme #=> "https"
34+
# uri.host #=> "github.com"
35+
# uri.path #=> "/ruby-git/ruby-git.git"
36+
#
37+
# Git::URL.parse('/Users/James/projects/ruby-git')
38+
# #=> #<Addressable::URI:0x438 URI:/Users/James/projects/ruby-git>
39+
#
40+
# @param url [String] the Git URL to parse
41+
#
42+
# @return [Addressable::URI] the parsed URI
43+
#
44+
def self.parse(url)
45+
if !url.start_with?('file:') && (m = GIT_ALTERNATIVE_SSH_SYNTAX.match(url))
46+
GitAltURI.new(user: m[:user], host: m[:host], path: m[:path])
47+
else
48+
Addressable::URI.parse(url)
49+
end
50+
end
51+
52+
# The name `git clone` would use for the repository directory for the given URL
53+
#
54+
# @example
55+
# Git::URL.clone_to('https://github.com/ruby-git/ruby-git.git') #=> 'ruby-git'
56+
#
57+
# @param url [String] the Git URL containing the repository directory
58+
#
59+
# @return [String] the name of the repository directory
60+
#
61+
def self.clone_to(url)
62+
uri = parse(url)
63+
path_parts = uri.path.split('/')
64+
path_parts.pop if path_parts.last == '.git'
65+
66+
path_parts.last.sub(/\.git$/, '')
67+
end
68+
end
69+
70+
# The URI for git's alternative scp-like syntax
71+
#
72+
# This class is necessary to ensure that #to_s returns the same string
73+
# that was passed to the initializer.
74+
#
75+
# @api public
76+
#
77+
class GitAltURI < Addressable::URI
78+
# Create a new GitAltURI object
79+
#
80+
# @example
81+
# uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
82+
# uri.to_s #=> 'james@github.com/james/ruby-git'
83+
#
84+
# @param user [String, nil] the user from the URL or nil
85+
# @param host [String] the host from the URL
86+
# @param path [String] the path from the URL
87+
#
88+
def initialize(user:, host:, path:)
89+
super(scheme: 'git-alt', user: user, host: host, path: path)
90+
end
91+
92+
# Convert the URI to a String
93+
#
94+
# @example
95+
# uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
96+
# uri.to_s #=> 'james@github.com/james/ruby-git'
97+
#
98+
# @return [String] the URI as a String
99+
#
100+
def to_s
101+
if user
102+
"#{user}@#{host}:#{path[1..-1]}"
103+
else
104+
"#{host}:#{path[1..-1]}"
105+
end
106+
end
107+
end
108+
end

tests/units/test_url.rb

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
require 'test/unit'
2+
3+
GIT_URLS = [
4+
{
5+
url: 'ssh://host.xz/path/to/repo.git/',
6+
expected_attributes: { scheme: 'ssh', host: 'host.xz', path: '/path/to/repo.git/' },
7+
expected_clone_to: 'repo'
8+
},
9+
{
10+
url: 'ssh://host.xz:4443/path/to/repo.git/',
11+
expected_attributes: { scheme: 'ssh', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
12+
expected_clone_to: 'repo'
13+
},
14+
{
15+
url: 'ssh:///path/to/repo.git/',
16+
expected_attributes: { scheme: 'ssh', host: '', path: '/path/to/repo.git/' },
17+
expected_clone_to: 'repo'
18+
},
19+
{
20+
url: 'user@host.xz:path/to/repo.git/',
21+
expected_attributes: { scheme: 'git-alt', user: 'user', host: 'host.xz', path: '/path/to/repo.git/' },
22+
expected_clone_to: 'repo'
23+
},
24+
{
25+
url: 'host.xz:path/to/repo.git/',
26+
expected_attributes: { scheme: 'git-alt', host: 'host.xz', path: '/path/to/repo.git/' },
27+
expected_clone_to: 'repo'
28+
},
29+
{
30+
url: 'git://host.xz:4443/path/to/repo.git/',
31+
expected_attributes: { scheme: 'git', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
32+
expected_clone_to: 'repo'
33+
},
34+
{
35+
url: 'git://user@host.xz:4443/path/to/repo.git/',
36+
expected_attributes: { scheme: 'git', user: 'user', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
37+
expected_clone_to: 'repo'
38+
},
39+
{
40+
url: 'https://host.xz/path/to/repo.git/',
41+
expected_attributes: { scheme: 'https', host: 'host.xz', path: '/path/to/repo.git/' },
42+
expected_clone_to: 'repo'
43+
},
44+
{
45+
url: 'https://host.xz:4443/path/to/repo.git/',
46+
expected_attributes: { scheme: 'https', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
47+
expected_clone_to: 'repo'
48+
},
49+
{
50+
url: 'ftps://host.xz:4443/path/to/repo.git/',
51+
expected_attributes: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
52+
expected_clone_to: 'repo'
53+
},
54+
{
55+
url: 'ftps://host.xz:4443/path/to/repo.git/',
56+
expected_attributes: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' },
57+
expected_clone_to: 'repo'
58+
},
59+
{
60+
url: 'file:./relative-path/to/repo.git/',
61+
expected_attributes: { scheme: 'file', path: './relative-path/to/repo.git/' },
62+
expected_clone_to: 'repo'
63+
},
64+
{
65+
url: 'file:///path/to/repo.git/',
66+
expected_attributes: { scheme: 'file', host: '', path: '/path/to/repo.git/' },
67+
expected_clone_to: 'repo'
68+
},
69+
{
70+
url: 'file:///path/to/repo.git',
71+
expected_attributes: { scheme: 'file', host: '', path: '/path/to/repo.git' },
72+
expected_clone_to: 'repo'
73+
},
74+
{
75+
url: 'file://host.xz/path/to/repo.git',
76+
expected_attributes: { scheme: 'file', host: 'host.xz', path: '/path/to/repo.git' },
77+
expected_clone_to: 'repo'
78+
},
79+
{
80+
url: '/path/to/repo.git/',
81+
expected_attributes: { path: '/path/to/repo.git/' },
82+
expected_clone_to: 'repo'
83+
},
84+
{
85+
url: '/path/to/bare-repo/.git',
86+
expected_attributes: { path: '/path/to/bare-repo/.git' },
87+
expected_clone_to: 'bare-repo'
88+
},
89+
{
90+
url: 'relative-path/to/repo.git/',
91+
expected_attributes: { path: 'relative-path/to/repo.git/' },
92+
expected_clone_to: 'repo'
93+
},
94+
{
95+
url: './relative-path/to/repo.git/',
96+
expected_attributes: { path: './relative-path/to/repo.git/' },
97+
expected_clone_to: 'repo'
98+
},
99+
{
100+
url: '../ruby-git/.git',
101+
expected_attributes: { path: '../ruby-git/.git' },
102+
expected_clone_to: 'ruby-git'
103+
}
104+
].freeze
105+
106+
# Tests for the Git::URL class
107+
#
108+
class TestURL < Test::Unit::TestCase
109+
def test_parse
110+
GIT_URLS.each do |url_data|
111+
url = url_data[:url]
112+
expected_attributes = url_data[:expected_attributes]
113+
actual_attributes = Git::URL.parse(url).to_hash.delete_if {| key, value | value.nil? }
114+
assert_equal(expected_attributes, actual_attributes, "Failed to parse URL '#{url}' correctly")
115+
end
116+
end
117+
118+
def test_clone_to
119+
GIT_URLS.each do |url_data|
120+
url = url_data[:url]
121+
expected_clone_to = url_data[:expected_clone_to]
122+
actual_repo_name = Git::URL.clone_to(url)
123+
assert_equal(
124+
expected_clone_to, actual_repo_name,
125+
"Failed to determine the repository directory for URL '#{url}' correctly"
126+
)
127+
end
128+
end
129+
130+
def test_to_s
131+
GIT_URLS.each do |url_data|
132+
url = url_data[:url]
133+
to_s = Git::URL.parse(url).to_s
134+
assert_equal(url, to_s, "Parsed URI#to_s does not return the original URL '#{url}' correctly")
135+
end
136+
end
137+
end

0 commit comments

Comments
 (0)