Skip to content

Commit a2a1ab8

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

File tree

5 files changed

+288
-0
lines changed

5 files changed

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

tests/units/test_git_alt_uri.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require 'test/unit'
2+
3+
# Tests for the Git::GitAltURI class
4+
#
5+
class TestGitAltURI < Test::Unit::TestCase
6+
def test_new
7+
uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'ruby-git/ruby-git.git')
8+
actual_attributes = uri.to_hash.delete_if { |_key, value| value.nil? }
9+
expected_attributes = {
10+
scheme: 'git-alt',
11+
user: 'james',
12+
host: 'github.com',
13+
path: '/ruby-git/ruby-git.git'
14+
}
15+
assert_equal(expected_attributes, actual_attributes)
16+
end
17+
18+
def test_to_s
19+
uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'ruby-git/ruby-git.git')
20+
assert_equal('james@github.com:ruby-git/ruby-git.git', uri.to_s)
21+
end
22+
23+
def test_to_s_with_nil_user
24+
uri = Git::GitAltURI.new(user: nil, host: 'github.com', path: 'ruby-git/ruby-git.git')
25+
assert_equal('github.com:ruby-git/ruby-git.git', uri.to_s)
26+
end
27+
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy