diff --git a/script/add-grammar b/script/add-grammar index 5a0b0b81..59248441 100755 --- a/script/add-grammar +++ b/script/add-grammar @@ -3,27 +3,102 @@ require "optparse" require "open3" -ROOT = File.expand_path("../../", __FILE__) +class GrammarRepo + + # Whitelist of trusted hosting providers + HOSTS = Regexp.union %w[github.com bitbucket.org gitlab.com] + + # Public: Define a repository source by upstream URL. + # + # url - an HTTPS, HTTP, or SSH address accepted by git-remote(1) + # Only domains listed in HOSTS are accepted; unrecognised + # hostnames or invalid URLs will raise an ArgumentError. + # + # Assumption: Repo URLs will never include subdomains. + # We only check for a possible `www`, nothing else. + # + # module_path - path of submodule as registered in `.gitmodules` + # Omit this unless grammar is being replaced. + def initialize(url, module_path = nil) + if https? url + @host = $1.downcase + @user = $2 + @repo = $3.sub /\.git$/, "" + elsif ssh?(url) || shorthand?(url) + @host = $1.downcase + @user = $2 + @repo = $3 + elsif implicit_shorthand? url + @host = "github.com" + @user = $1 + @repo = $2 + else + raise ArgumentError, "Unsupported URL: #{url}" + end + end + + # Match a well-formed HTTP or HTTPS address + def https?(url) + nil unless url =~ / + ^ (? https? ://)? + (? [^@.]+ @ )? + (? www \. )? + (? #{HOSTS} ) + \/ (? [^\/]+ ) + \/ (? [^\/]+ ) /xi + end + + # Match an SSH address starting with `git@` + def ssh?(url) + nil unless url =~ / + ^ git@ + (? #{HOSTS}) : + (? [^\/]+) \/ + (? [^\/]+) \.git $/xi + end + + # Match `provider:user/repo` + def shorthand?(url) + nil unless url =~ / + ^ (? #{HOSTS}) : \/? + (? [^\/]+) \/ + (? [^\/]+) \/? $ /xi + end + + # Match `user/repo` shorthand, assumed to be GitHub + def implicit_shorthand?(url) + nil unless url =~ / + ^ \/? (?[^\/]+) + \/ (?[^\/]+) + \/? $/xi + end +end -# Break a repository URL into its separate components -def parse_url(input) - hosts = "github\.com|bitbucket\.org|gitlab\.com" +class GrammarGuardian - # HTTPS/HTTP link pointing to recognised hosts - if input =~ /^(?:https?:\/\/)?(?:[^.@]+@)?(?:www\.)?(#{hosts})\/([^\/]+)\/([^\/]+)/i - { host: $1.downcase(), user: $2, repo: $3.sub(/\.git$/, "") } - # SSH - elsif input =~ /^git@(#{hosts}):([^\/]+)\/([^\/]+)\.git$/i - { host: $1.downcase(), user: $2, repo: $3 } - # provider:user/repo - elsif input =~ /^(github|bitbucket|gitlab):\/?([^\/]+)\/([^\/]+)\/?$/i - { host: $1.downcase(), user: $2, repo: $3 } - # user/repo - Common GitHub shorthand - elsif input =~ /^\/?([^\/]+)\/([^\/]+)\/?$/ - { host: "github.com", user: $1, repo: $2 } - else - raise "Unsupported URL: #{input}" + ROOT = File.expand_path("../../", __FILE__) + + def initialize + # Track each change so we can roll back after a failed command + @changes = Hash.new + end + + # Print debugging feedback to STDOUT if running with --verbose + def log(msg) + puts msg if $verbose + end + + def command(*args) + log "$ #{args.join(' ')}" + output, status = Open3.capture2e(*args) + if !status.success? + output.each_line do |line| + log " > #{line}" + end + warn "Command failed. Aborting." + exit 1 + end end end @@ -38,22 +113,6 @@ def parse_submodule(name) path end -# Print debugging feedback to STDOUT if running with --verbose -def log(msg) - puts msg if $verbose -end - -def command(*args) - log "$ #{args.join(' ')}" - output, status = Open3.capture2e(*args) - if !status.success? - output.each_line do |line| - log " > #{line}" - end - warn "Command failed. Aborting." - exit 1 - end -end usage = """Usage: #{$0} [-v|--verbose] [--replace grammar] url