#!/usr/bin/env ruby require "optparse" require "open3" 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 class GrammarGuardian 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 # Isolate the vendor-name component of a submodule path def parse_submodule(name) name =~ /^(?:.*(?:vendor\/)?grammars\/)?([^\/]+)/i path = "vendor/grammars/#{$1}" unless File.exist?("#{ROOT}/" + path) warn "Submodule '#{path}' does not exist. Aborting." exit 1 end path end usage = """Usage: #{$0} [-v|--verbose] [--replace grammar] url Examples: #{$0} https://github.com/Alhadis/language-roff #{$0} --replace sublime-apl https://github.com/Alhadis/language-apl """ $replace = nil $verbose = true $compile = false OptionParser.new do |opts| opts.banner = usage opts.on("-q", "--quiet", "Do not print output unless there's a failure") do $verbose = false end opts.on("-rSUBMODULE", "--replace=SUBMODDULE", "Replace an existing grammar submodule.") do |name| $replace = name end opts.on("-C", "--compile", "Compile grammar using the new grammar-compiler.") do $compile = true end end.parse! $url = ARGV[0] # No URL? Print a usage message and bail. unless $url warn usage exit 1; end # Ensure the given URL is an HTTPS link parts = parse_url $url https = "https://#{parts[:host]}/#{parts[:user]}/#{parts[:repo]}" repo_new = "vendor/grammars/#{parts[:repo]}" repo_old = parse_submodule($replace) if $replace Dir.chdir(ROOT) if repo_old log "Deregistering: #{repo_old}" command('git', 'submodule', 'deinit', repo_old) command('git', 'rm', '-rf', repo_old) command('script/grammar-compiler', 'update', '-f') if $compile end log "Registering new submodule: #{repo_new}" command('git', 'submodule', 'add', '-f', https, repo_new) command('script/grammar-compiler', 'add', repo_new) if $compile log "Confirming license" if repo_old command('script/licensed') else repo_new = File.absolute_path(repo_new) command('script/licensed', '--module', repo_new) end log "Updating grammar documentation in vendor/README.md" command('bundle', 'exec', 'rake', 'samples') command('script/sort-submodules') command('script/list-grammars')