Coming from Python, JavaScript and PHP, I'd like to learn to write Ruby in the way it's "supposed to". Well-written Python code is called "Pythonic", so I'd like to know how idiomatic my Ruby code is.
I've had great help from Rubocop, slapping my face if I didn't write properly, but I still feel some things can be done better.
Application
This set of scripts downloads a github/gitlab/bitbucket repository, removes the .git folder and moves it to the specified folder, so that the files are "de-git".
Some commands:
# Run tests (for regexes)
./degit.rb --test
# Extract repo to folder with repo name
./degit.rb zpqrtbnk/test-repo
# Extract tag/branch of repo to folder with repo name
./degit.rb zpqrtbnk/test-repo#temp
# Extract repo to specified folder
./degit.rb zpqrtbnk/test-repo some-folder
Code
Ruby v2.4.5
Idea came from degit by Rich Harris
degit.rb
#!/usr/bin/env ruby
require "tmpdir"
require_relative "repo"
require_relative "repo_type"
REPO_TYPES = {
github: RepoType.new("github", "https://github.com", "github.com"),
gitlab: RepoType.new("gitlab", "https://gitlab.com", "gitlab.com"),
bitbucket: RepoType.new("bitbucket", "https://bitbucket.org", "bitbucket.org"),
custom: RepoType.new("custom", :custom, :custom),
}.freeze
# TODO: Error handling
def main
repo_name = ARGV[0]
folder_name = ARGV[1]
raise "Required parameter repo name not specified" if repo_name.nil?
if repo_name == "--test"
require_relative "tests"
run_tests
return
end
degit repo_name, folder_name
end
def temp_dir
dir = Dir.mktmpdir("degit-", "/tmp")
at_exit { FileUtils.remove_entry(dir) }
dir
end
def degit(repo_name, folder_name)
repo = Repo.new repo_name
folder_name ||= repo.name
dest_dir = File.join Dir.pwd, folder_name
dir_exists = Dir.exist? dest_dir
if dir_exists
abort "Aborted" unless confirm_overwrite dest_dir
end
dir = temp_dir
tmp_repo_path = File.join(dir, folder_name)
cmd = repo.download_command tmp_repo_path
puts `#{cmd}`
FileUtils.remove_entry File.join(tmp_repo_path, ".git")
FileUtils.remove_entry dest_dir if dir_exists
FileUtils.mv(tmp_repo_path, Dir.pwd, force: true)
end
def confirm_overwrite(dest_dir)
print "Destination folder #{dest_dir} already exists. Overwrite folder? [y/n] "
# ARGV interferes with gets, so use STDIN.gets
input = STDIN.gets.chomp.downcase
return (input == "y") if %w[y n].include? input
# Continue to ask until input is either y or n
confirm_overwrite dest_dir
end
main if $PROGRAM_NAME == __FILE__
repo_type.rb
class RepoType
attr_reader :name, :full_url
def initialize(name, full_url, base_url, short_code=nil)
@name = name
@full_url = full_url
@base_url = base_url
@short_code = short_code || name.to_s.downcase
end
def id?(id)
[@short_code, @base_url].include? id
end
end
repo.rb
class Repo
attr_reader :type, :tag, :name
PREFIX_REGEX = %r{
\A
((?<type>github|gitlab|bitbucket):)?
(?<owner>[\w-]+)/(?<name>[\w-]+)
(\#(?<tag>[\w\-\.]+))?
\z
}xi.freeze
SSH_REGEX = %r{
\A
(?<source_url>
git@(?<type>github\.com|gitlab\.com|bitbucket\.org):
(?<owner>[\w-]+)/(?<name>[\w-]+)
(\.git)?
)
(\#(?<tag>[\w\-\.]+))?
\z
}xi.freeze
HTTPS_REGEX = %r{
\A
(?<source_url>
https://(?<type>github\.com|gitlab\.com|bitbucket\.org)/
(?<owner>[\w-]+)/(?<name>[\w-]+)
)
(\#(?<tag>[\w\-\.]+))?
\z
}xi.freeze
def initialize(uri)
@uri = uri
raise "Required constant REPO_TYPES not defined" unless defined? REPO_TYPES
parse_uri
# debug unless @source_url.nil?
end
def valid_uri?
@uri.end_with?(".git") || @uri.include?("/")
end
def parse_uri
if @uri.end_with? ".git"
@type = REPO_TYPES[:custom]
return
end
repo = match_repo_info
return nil if repo.nil?
@owner = repo[:owner]
@name = repo[:name]
@tag = repo[:tag]
@source_url = make_source_url repo
end
def match_repo_info
[PREFIX_REGEX, SSH_REGEX, HTTPS_REGEX].each do |regex|
repo_matches = regex.match @uri
unless repo_matches.nil?
@type = find_repo_type repo_matches[:type]
return repo_matches
end
end
nil
end
def find_repo_type(type)
REPO_TYPES.each do |_, repo_type|
return repo_type if repo_type.id? type
end
REPO_TYPES[:github]
end
def make_source_url(repo)
return repo[:source_url] if repo.names.include? "source_url"
source_url = @type.full_url || @uri
"#{source_url}/#{@owner}/#{@name}"
end
def download_command(output_folder=nil)
tag_spec = @tag.nil? ? "" : "--branch #{@tag}"
parts = [
"git clone --quiet --depth 1",
tag_spec,
@source_url,
output_folder || @name,
]
parts.join " "
end
def debug
puts ""
puts "source_url: #{@source_url}" unless @source_url.nil?
puts "owner: #{@owner}" unless @owner.nil?
puts "name: #{@name}" unless @name.nil?
puts "tag: #{@tag}" unless @tag.nil?
puts "download cmd: #{download_command}"
end
end
tests.rb
VALID = %w[
user1/repo1
github:user2/repo2
[email protected]:user3/repo3
https://github.com/rmccue/test-repository
gitlab:user5/repo5
[email protected]:user6/repo6
https://gitlab.com/user7/repo7
bitbucket:user8/repo8
[email protected]:user9/repo9
https://bitbucket.org/user0/repo0
].freeze
INVALID = %w[
http://github.com/user1/repo1
https://github.com/user2
https://github.comuser3/repo3
].freeze
WITH_TAG = %w[
user1/repo1#dev
user2/repo2#v1.2.3
user3/repo3#1234abcd
].freeze
WITH_GIT_SUFFIX = %w[
https://github.com/Rich-Harris/degit.git
user@host:~/repos/website.nl.git
].freeze
def pf(str)
print str
$stdout.flush
end
def run_tests
pf " VALID: "
VALID.each do |r|
pf "."
repo = Repo.new r
raise "#{r} isn't valid" if repo.type.nil?
end
puts ""
pf " INVALID: "
INVALID.each do |r|
pf "."
repo = Repo.new r
raise "#{r} isn't invalid" unless repo.type.nil?
end
puts ""
pf " WITH_TAG: "
WITH_TAG.each do |r|
pf "."
repo = Repo.new r
raise "#{r} isn't valid" if repo.type.nil?
raise "#{r} has no tag" if repo.tag.nil?
end
puts ""
pf "WITH_GIT_SUFFIX: "
WITH_GIT_SUFFIX.each do |r|
pf "."
repo = Repo.new r
raise "#{r} isn't valid" if repo.type.nil?
end
puts ""
end