Skip to content

Commit 37182c7

Browse files
authored
Merge branch 'master' into default_branch
2 parents f0300df + 45b467c commit 37182c7

10 files changed

+443
-13
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,23 @@ g = Git.init
206206
{ :repository => '/opt/git/proj.git',
207207
:index => '/tmp/index'} )
208208

209-
g = Git.clone(URI, NAME, :path => '/tmp/checkout')
209+
# Clone from a git url
210+
git_url = 'https://github.com/ruby-git/ruby-git.git'
211+
# Clone into the ruby-git directory
212+
g = Git.clone(git_url)
213+
214+
# Clone into /tmp/clone/ruby-git-clean
215+
name = 'ruby-git-clean'
216+
path = '/tmp/clone'
217+
g = Git.clone(git_url, name, :path => path)
218+
g.dir #=> /tmp/clone/ruby-git-clean
219+
210220
g.config('user.name', 'Scott Chacon')
211221
g.config('user.email', 'email@email.com')
212222

213223
# Clone can take an optional logger
214224
logger = Logger.new
215-
g = Git.clone(URI, NAME, :log => logger)
225+
g = Git.clone(git_url, NAME, :log => logger)
216226

217227
g.add # git add -- "."
218228
g.add(:all=>true) # git add --all -- "."

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: 19 additions & 4 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'
@@ -106,11 +107,23 @@ def self.bare(git_dir, options = {})
106107
# @see https://git-scm.com/docs/git-clone git clone
107108
# @see https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a GIT URLs
108109
#
109-
# @param [URI, Pathname] repository The (possibly remote) repository to clone
110+
# @param repository_url [URI, Pathname] The (possibly remote) repository url to clone
110111
# from. See [GIT URLS](https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a)
111112
# for more information.
112113
#
113-
# @param [Pathname] name The directory to clone into.
114+
# @param directory [Pathname, nil] The directory to clone into
115+
#
116+
# If `directory` is a relative directory it is relative to the `path` option if
117+
# given. If `path` is not given, `directory` is relative to the current working
118+
# directory.
119+
#
120+
# If `nil`, `directory` will be set to the basename of the last component of
121+
# the path from the `repository_url`. For example, for the URL:
122+
# `https://github.com/org/repo.git`, `directory` will be set to `repo`.
123+
#
124+
# If the last component of the path is `.git`, the next-to-last component of
125+
# the path is used. For example, for the URL `/Users/me/foo/.git`, `directory`
126+
# will be set to `foo`.
114127
#
115128
# @param [Hash] options The options for this command (see list of valid
116129
# options below)
@@ -157,8 +170,10 @@ def self.bare(git_dir, options = {})
157170
# @return [Git::Base] an object that can execute git commands in the context
158171
# of the cloned local working copy or cloned repository.
159172
#
160-
def self.clone(repository, name, options = {})
161-
Base.clone(repository, name, options)
173+
def self.clone(repository_url, directory = nil, options = {})
174+
clone_to_options = options.select { |key, _value| %i[bare mirror].include?(key) }
175+
directory ||= Git::URL.clone_to(repository_url, **clone_to_options)
176+
Base.clone(repository_url, directory, options)
162177
end
163178

164179
# Returns the name of the default branch of the given repository

lib/git/base.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ def self.bare(git_dir, options = {})
1717
end
1818

1919
# (see Git.clone)
20-
def self.clone(repository, name, options = {})
21-
new_options = Git::Lib.new(nil, options[:log]).clone(repository, name, options)
20+
def self.clone(repository_url, directory, options = {})
21+
new_options = Git::Lib.new(nil, options[:log]).clone(repository_url, directory, options)
2222
normalize_paths(new_options, bare: options[:bare] || options[:mirror])
23-
self.new(new_options)
23+
new(new_options)
2424
end
2525

2626
# (see Git.default_branch)

lib/git/lib.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ def init(opts={})
9595
#
9696
# @return [Hash] the options to pass to {Git::Base.new}
9797
#
98-
def clone(repository, name, opts = {})
98+
def clone(repository_url, directory, opts = {})
9999
@path = opts[:path] || '.'
100-
clone_dir = opts[:path] ? File.join(@path, name) : name
100+
clone_dir = opts[:path] ? File.join(@path, directory) : directory
101101

102102
arr_opts = []
103103
arr_opts << '--bare' if opts[:bare]
@@ -106,11 +106,11 @@ def clone(repository, name, opts = {})
106106
arr_opts << '--config' << opts[:config] if opts[:config]
107107
arr_opts << '--origin' << opts[:remote] || opts[:origin] if opts[:remote] || opts[:origin]
108108
arr_opts << '--recursive' if opts[:recursive]
109-
arr_opts << "--mirror" if opts[:mirror]
109+
arr_opts << '--mirror' if opts[:mirror]
110110

111111
arr_opts << '--'
112112

113-
arr_opts << repository
113+
arr_opts << repository_url
114114
arr_opts << clone_dir
115115

116116
command('clone', arr_opts)

lib/git/url.rb

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 directory `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, bare: false, mirror: false)
65+
uri = parse(url)
66+
path_parts = uri.path.split('/')
67+
path_parts.pop if path_parts.last == '.git'
68+
directory = path_parts.last
69+
if bare || mirror
70+
directory += '.git' unless directory.end_with?('.git')
71+
elsif directory.end_with?('.git')
72+
directory = directory[0..-5]
73+
end
74+
directory
75+
end
76+
end
77+
78+
# The URI for git's alternative scp-like syntax
79+
#
80+
# This class is necessary to ensure that #to_s returns the same string
81+
# that was passed to the initializer.
82+
#
83+
# @api public
84+
#
85+
class GitAltURI < Addressable::URI
86+
# Create a new GitAltURI object
87+
#
88+
# @example
89+
# uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
90+
# uri.to_s #=> 'james@github.com/james/ruby-git'
91+
#
92+
# @param user [String, nil] the user from the URL or nil
93+
# @param host [String] the host from the URL
94+
# @param path [String] the path from the URL
95+
#
96+
def initialize(user:, host:, path:)
97+
super(scheme: 'git-alt', user: user, host: host, path: path)
98+
end
99+
100+
# Convert the URI to a String
101+
#
102+
# Addressible::URI forces path to be absolute by prepending a '/' to the
103+
# path. This method removes the '/' when converting back to a string
104+
# since that is what is expected by git. The following is a valid git URL:
105+
#
106+
# `james@github.com:ruby-git/ruby-git.git`
107+
#
108+
# and the following (with the initial '/'' in the path) is NOT a valid git URL:
109+
#
110+
# `james@github.com:/ruby-git/ruby-git.git`
111+
#
112+
# @example
113+
# uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
114+
# uri.path #=> '/james/ruby-git'
115+
# uri.to_s #=> 'james@github.com:james/ruby-git'
116+
#
117+
# @return [String] the URI as a String
118+
#
119+
def to_s
120+
if user
121+
"#{user}@#{host}:#{path[1..-1]}"
122+
else
123+
"#{host}:#{path[1..-1]}"
124+
end
125+
end
126+
end
127+
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_git_clone.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
require 'test/unit'
4+
require_relative '../test_helper'
5+
6+
# Tests for Git.clone
7+
class TestGitClone < Test::Unit::TestCase
8+
def setup_repo
9+
Git.init('repository.git', bare: true)
10+
git = Git.clone('repository.git', 'temp')
11+
File.write('temp/test.txt', 'test')
12+
git.add('test.txt')
13+
git.commit('Initial commit')
14+
end
15+
16+
def test_git_clone_with_name
17+
in_temp_dir do |path|
18+
setup_repo
19+
clone_dir = 'clone_to_this_dir'
20+
git = Git.clone('repository.git', clone_dir)
21+
assert(Dir.exist?(clone_dir))
22+
expected_dir = File.realpath(clone_dir)
23+
assert_equal(expected_dir, git.dir.to_s)
24+
end
25+
end
26+
27+
def test_git_clone_with_no_name
28+
in_temp_dir do |path|
29+
setup_repo
30+
git = Git.clone('repository.git')
31+
assert(Dir.exist?('repository'))
32+
expected_dir = File.realpath('repository')
33+
assert_equal(expected_dir, git.dir.to_s)
34+
end
35+
end
36+
end

0 commit comments

Comments
 (0)