├── examples ├── README.md └── hello_world │ ├── README.md │ ├── resourceful.txt │ ├── simple.rb │ ├── method.gisture │ ├── method.rb │ ├── called_method.rb │ ├── class.gist │ ├── gisture.yml │ ├── resourceful.gist │ ├── resourceful.rb │ ├── array.gist │ ├── class.rb │ ├── multi.gist │ └── called_class.rb ├── lib ├── gisture │ ├── version.rb │ ├── repo │ │ ├── gists.rb │ │ ├── file.rb │ │ └── gist.rb │ ├── strategies │ │ ├── load.rb │ │ ├── require.rb │ │ ├── include.rb │ │ ├── eval.rb │ │ ├── base.rb │ │ ├── tempfile.rb │ │ └── exec.rb │ ├── strategies.rb │ ├── github_api │ │ └── client │ │ │ └── gists.rb │ ├── evaluator.rb │ ├── errors.rb │ ├── railtie.rb │ ├── file │ │ └── cloned.rb │ ├── cloneable.rb │ ├── repo.rb │ ├── gist.rb │ └── file.rb ├── tasks │ └── gisture.rake └── gisture.rb ├── .gitignore ├── Gemfile ├── .travis.yml ├── spec ├── gisture │ ├── file_spec.rb │ ├── evaluator_spec.rb │ ├── repo_spec.rb │ └── gist_spec.rb ├── support │ └── github_api.rb ├── gisture_spec.rb └── spec_helper.rb ├── Rakefile ├── gisture.gemspec ├── bin └── gisture └── README.md /examples/README.md: -------------------------------------------------------------------------------- 1 | # Gisture Examples 2 | -------------------------------------------------------------------------------- /examples/hello_world/README.md: -------------------------------------------------------------------------------- 1 | # Gisture Examples 2 | -------------------------------------------------------------------------------- /lib/gisture/version.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | VERSION = '0.0.13' 3 | end 4 | -------------------------------------------------------------------------------- /examples/hello_world/resourceful.txt: -------------------------------------------------------------------------------- 1 | This is the content of resourceful.txt 2 | -------------------------------------------------------------------------------- /examples/hello_world/simple.rb: -------------------------------------------------------------------------------- 1 | 10.times do 2 | puts "Hello World!" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | Gemfile.lock 3 | *.swp 4 | .rspec 5 | coverage 6 | spec/cassettes 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | 4 | gem 'coveralls', require: false 5 | -------------------------------------------------------------------------------- /examples/hello_world/method.gisture: -------------------------------------------------------------------------------- 1 | path: examples/hello_world/method.rb 2 | strategy: eval 3 | clone: false 4 | -------------------------------------------------------------------------------- /examples/hello_world/method.rb: -------------------------------------------------------------------------------- 1 | def hello_world 2 | 10.times do 3 | puts "Hello World!" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.7 4 | - 2.3.4 5 | - 2.4.1 6 | branches: 7 | only: 8 | - master 9 | -------------------------------------------------------------------------------- /examples/hello_world/called_method.rb: -------------------------------------------------------------------------------- 1 | def hello_world 2 | 10.times do 3 | puts "Hello World!" 4 | end 5 | end 6 | 7 | hello_world 8 | -------------------------------------------------------------------------------- /examples/hello_world/class.gist: -------------------------------------------------------------------------------- 1 | path: examples/hello_world/class.rb 2 | strategy: require 3 | evaluator: "Gisture::Evaluator" 4 | clone: true 5 | -------------------------------------------------------------------------------- /examples/hello_world/gisture.yml: -------------------------------------------------------------------------------- 1 | path: examples/hello_world/simple.rb 2 | strategy: eval 3 | evaluator: "Gisture::Evaluator" 4 | executor: ['ruby', '-v'] 5 | clone: true 6 | -------------------------------------------------------------------------------- /examples/hello_world/resourceful.gist: -------------------------------------------------------------------------------- 1 | path: examples/hello_world/resourceful.rb 2 | resources: 3 | - examples/hello_world/resourceful.txt 4 | - lib/gisture/version.rb 5 | -------------------------------------------------------------------------------- /lib/gisture/repo/gists.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class Repo::Gists < Array 3 | def run!(*args, &block) 4 | map { |gist| gist.run!(*args, &block) } 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /examples/hello_world/resourceful.rb: -------------------------------------------------------------------------------- 1 | # example of reading a resource that was included in a .gist 2 | puts ::File.read('examples/hello_world/resourceful.txt') 3 | puts ::File.read('lib/gisture/version.rb') 4 | -------------------------------------------------------------------------------- /examples/hello_world/array.gist: -------------------------------------------------------------------------------- 1 | gists: 2 | - path: examples/hello_world/simple.rb 3 | strategy: eval 4 | - path: examples/hello_world/method.rb 5 | strategy: load 6 | - path: examples/hello_world/class.rb 7 | strategy: require 8 | -------------------------------------------------------------------------------- /lib/gisture/strategies/load.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | module Strategies 3 | class Load < Tempfile 4 | include Include 5 | 6 | def run!(*args, &block) 7 | include!(:load, *args, &block) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /examples/hello_world/class.rb: -------------------------------------------------------------------------------- 1 | class GistTestClass 2 | def self.hello_world 3 | 10.times do 4 | puts "Hello World!" 5 | end 6 | end 7 | 8 | def hello_world 9 | 10.times do 10 | puts "Hello World!" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/gisture/strategies/require.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | module Strategies 3 | class Require < Tempfile 4 | include Include 5 | 6 | def run!(*args, &block) 7 | include!(:require, *args, &block) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/gisture/strategies.rb: -------------------------------------------------------------------------------- 1 | require 'gisture/strategies/base' 2 | require 'gisture/strategies/tempfile' 3 | require 'gisture/strategies/include' 4 | require 'gisture/strategies/eval' 5 | require 'gisture/strategies/exec' 6 | require 'gisture/strategies/require' 7 | require 'gisture/strategies/load' 8 | -------------------------------------------------------------------------------- /examples/hello_world/multi.gist: -------------------------------------------------------------------------------- 1 | name: multi-gist 2 | gistures: 3 | simple: 4 | path: examples/hello_world/simple.rb 5 | strategy: eval 6 | method: 7 | path: examples/hello_world/method.rb 8 | strategy: load 9 | class: 10 | path: examples/hello_world/class.rb 11 | strategy: require 12 | -------------------------------------------------------------------------------- /lib/gisture/strategies/include.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | module Strategies 3 | module Include 4 | def include!(strat, *args, &block) 5 | log! 6 | included = eval("#{strat} tempfile.path") 7 | unlink! 8 | block_given? ? yield : included 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/gisture/github_api/client/gists.rb: -------------------------------------------------------------------------------- 1 | require 'github_api/client/gists' 2 | 3 | module Github 4 | class Client::Gists < API 5 | def version(*args) 6 | arguments(args, required: [:id, :version]) 7 | 8 | get_request("/gists/#{arguments.id}/#{arguments.version}", arguments.params) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/gisture/evaluator.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class Evaluator 3 | attr_reader :raw, :result 4 | 5 | def evaluate(&block) 6 | instance_eval { @result = eval raw } 7 | instance_eval &block if block_given? 8 | self 9 | end 10 | 11 | protected 12 | 13 | def initialize(raw) 14 | @raw = raw.to_s 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/gisture/errors.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class OwnerBlacklisted < StandardError 3 | def initialize(owner) 4 | super("Gists from '#{owner}' have not been whitelisted for execution. Add them to the 'owners' configuration option.") 5 | end 6 | end 7 | 8 | class FileLocalizationError < StandardError; end 9 | class AmbiguousRepoFile < StandardError; end 10 | end 11 | -------------------------------------------------------------------------------- /lib/gisture/railtie.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class Railtie < Rails::Railtie 3 | initializer "gisture.configure_rails_logger" do 4 | Gisture.configure do |config| 5 | config.logger = Rails.logger 6 | end 7 | end 8 | 9 | rake_tasks do 10 | Dir[::File.join(::File.dirname(__FILE__),'../tasks/*.rake')].each { |f| load f } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/hello_world/called_class.rb: -------------------------------------------------------------------------------- 1 | class GistTestClass 2 | def self.hello_world 3 | 10.times do 4 | puts "Hello World!" 5 | end 6 | end 7 | 8 | def hello_world 9 | 10.times do 10 | puts "Hello World!" 11 | end 12 | end 13 | end 14 | 15 | # execute the defined methods as part of your gist 16 | GistTestClass.hello_world 17 | GistTestClass.new.hello_world 18 | -------------------------------------------------------------------------------- /spec/gisture/file_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Gisture::File do 4 | subject { Gisture::File.new(Gisture::Gist.new(TEST_GIST_ID).gist.files.first[1], slug: TEST_GIST_ID) } 5 | 6 | it "delegates missing methods to the file hash" do 7 | expect(subject.respond_to?(:content)).to be_true 8 | expect(subject.respond_to?(:filename)).to be_true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/tasks/gisture.rake: -------------------------------------------------------------------------------- 1 | require 'gisture' 2 | 3 | namespace :gisture do 4 | desc 'Run a gist or file in a rake task' 5 | task :run, [:url, :strategy, :filename, :version, :callback] => :environment do |t,args| 6 | callback = Proc.new { eval(args.callback.to_s) } 7 | Gisture.new(args.gist_id, strategy: args.strategy, filename: args.filename, version: args.version).run!(&callback) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/gisture/repo/file.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class Repo::File < File 3 | protected 4 | 5 | def initialize(file, basename: nil, root: nil, strategy: nil, evaluator: nil, executor: nil) 6 | file['filename'] = ::File.basename(file['path']) 7 | file['content'] = Base64.decode64(file['content']) 8 | super(file, basename: basename, root: root, strategy: strategy, evaluator: evaluator, executor: executor) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | load 'tasks/gisture.rake' 3 | 4 | task :environment do 5 | # noop 6 | end 7 | 8 | desc 'Run the specs' 9 | RSpec::Core::RakeTask.new do |r| 10 | r.verbose = false 11 | end 12 | 13 | desc "Open an irb session preloaded with this library" 14 | task :console do 15 | sh "irb -rubygems -I lib -r gisture" 16 | end 17 | 18 | task :build do 19 | puts `gem build gisture.gemspec` 20 | end 21 | 22 | task :push do 23 | require 'gisture/version' 24 | puts `gem push gisture-#{Gisture::VERSION}.gem` 25 | end 26 | 27 | task release: [:build, :push] do 28 | puts `rm -f gisture*.gem` 29 | end 30 | 31 | task :default => :spec 32 | -------------------------------------------------------------------------------- /lib/gisture/file/cloned.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class File::Cloned < File 3 | 4 | def unlink! 5 | false 6 | end 7 | 8 | def delocalize! 9 | false 10 | end 11 | 12 | protected 13 | 14 | def initialize(clone_path, file_path, basename: nil, strategy: nil, evaluator: nil, executor: nil) 15 | path = ::File.join(clone_path, file_path) 16 | @localized = ::File.new(path) 17 | file_hash = Hashie::Mash.new({path: path, filename: file_path, content: localized.read}) 18 | super(file_hash, basename: basename, root: clone_path, strategy: strategy, evaluator: evaluator, executor: executor) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gisture/strategies/eval.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | module Strategies 3 | class Eval < Base 4 | attr_reader :evaluator 5 | 6 | def run!(*args, &block) 7 | log! 8 | evalor(*args).evaluate(&block) 9 | end 10 | 11 | def evalor(*args) 12 | # push the default evaluator onto args so it gets used if no args were passed 13 | args << evaluator 14 | klass = eval(args.first.to_s) 15 | klass.new(content) 16 | end 17 | 18 | protected 19 | 20 | def initialize(content, filename: nil, slug: nil, evaluator: nil) 21 | super(content, slug: slug, filename: filename) 22 | @evaluator = evaluator || Gisture::Evaluator 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/gisture/strategies/base.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | module Strategies 3 | class Base 4 | attr_reader :content, :slug, :relpath, :filename, :extname 5 | 6 | def run_from!(path, *args, &block) 7 | Dir.chdir(path) { run!(*args, &block) } 8 | end 9 | alias_method :run_in!, :run_from! 10 | 11 | protected 12 | 13 | def initialize(content, filename: nil, slug: nil) 14 | @content = content 15 | @slug = slug || 'gisture/tmp' 16 | @relpath = filename || 'anonymous' 17 | @filename = ::File.basename(@relpath) 18 | @extname = ::File.extname(@filename) 19 | end 20 | 21 | def klass_name 22 | self.class.name.split('::').last.downcase 23 | end 24 | 25 | def log! 26 | Gisture.logger.info "[gisture] Running #{relpath} from #{Dir.pwd} via the :#{klass_name} strategy" 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /gisture.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "gisture/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "gisture" 6 | s.version = Gisture::VERSION 7 | s.summary = "Execute one-off gists inline or in the background." 8 | s.description = "Execute one-off gists inline or in the background." 9 | s.authors = ["Mark Rebec"] 10 | s.email = ["mark@markrebec.com"] 11 | s.homepage = "http://github.com/markrebec/gisture" 12 | 13 | s.files = Dir["lib/**/*"] 14 | s.test_files = Dir["spec/**/*"] 15 | s.executables = "gisture" 16 | 17 | s.add_dependency "canfig" 18 | s.add_dependency "hashie" 19 | s.add_dependency "git" 20 | s.add_dependency "github_api" 21 | s.add_dependency "commander" 22 | 23 | s.add_development_dependency "rake" 24 | s.add_development_dependency "rspec" 25 | s.add_development_dependency "vcr" 26 | s.add_development_dependency "webmock" 27 | end 28 | -------------------------------------------------------------------------------- /lib/gisture/strategies/tempfile.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | 3 | module Gisture 4 | module Strategies 5 | class Tempfile < Base 6 | 7 | def tempfile 8 | @tempfile ||= write_tempfile 9 | end 10 | 11 | def unlink! 12 | FileUtils.rm_f tempfile.path 13 | @tempfile = nil 14 | end 15 | 16 | protected 17 | 18 | def initialize(content, filename: nil, slug: nil, tempfile: nil) 19 | super(content, slug: slug, filename: filename) 20 | # allows overriding the tempfile with an existing cloned file path 21 | @tempfile = tempfile.is_a?(::File) ? tempfile : ::File.new(tempfile) unless tempfile.nil? 22 | end 23 | 24 | def write_tempfile 25 | tmpname = [slug.to_s.gsub(/\//, '-'), filename, extname].compact 26 | tmpfile = ::Tempfile.new(tmpname, Gisture.configuration.tmpdir) 27 | tmpfile.write(content) 28 | tmpfile.close 29 | tmpfile 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/gisture/cloneable.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | module Cloneable 3 | def clone_path 4 | @clone_path ||= ::File.join(Gisture.configuration.tmpdir, owner, name) 5 | end 6 | 7 | def clone!(&block) 8 | destroy_clone! 9 | clone(&block) 10 | end 11 | 12 | def clone(&block) 13 | return self if cloned? 14 | 15 | Gisture.logger.info "[gisture] Cloning #{owner}/#{name} into #{clone_path}" 16 | Git.clone(clone_url, name, path: ::File.dirname(clone_path)) 17 | stamp_clone! 18 | 19 | if block_given? 20 | instance_eval &block 21 | destroy_clone! 22 | end 23 | 24 | self 25 | end 26 | 27 | # removes the .git path and adds a .gisture stamp 28 | def stamp_clone! 29 | FileUtils.rm_rf("#{clone_path}/.git") 30 | ::File.write("#{clone_path}/.gisture", Time.now.to_i.to_s) 31 | end 32 | 33 | def destroy_clone! 34 | FileUtils.rm_rf(clone_path) 35 | end 36 | alias_method :destroy_cloned_files!, :destroy_clone! 37 | 38 | def cloned? 39 | ::File.read("#{clone_path}/.gisture").strip 40 | rescue 41 | false 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/gisture/evaluator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Gisture::Evaluator do 4 | it "requires a string to be eval'd" do 5 | expect { Gisture::Evaluator.new }.to raise_exception(ArgumentError) 6 | end 7 | 8 | it "converts raw input to a string" do 9 | expect(Gisture::Evaluator.new(nil).raw).to eql('') 10 | end 11 | 12 | describe "#raw" do 13 | subject { Gisture::Evaluator.new("1+1") } 14 | 15 | it "stores the raw string passed on init" do 16 | expect(subject.raw).to eql("1+1") 17 | end 18 | end 19 | 20 | describe "#evaluate" do 21 | subject { Gisture::Evaluator.new("@raw") } 22 | 23 | it "evaluates the input within the context of the instance" do 24 | subject.evaluate 25 | expect(subject.result).to eql("@raw") 26 | end 27 | 28 | it "returns itself" do 29 | expect(subject.evaluate).to eql(subject) 30 | end 31 | 32 | context "when passed a block" do 33 | it "evaluates the block within the context of the instance" do 34 | subject.evaluate { @raw = "changed" } 35 | expect(subject.raw).to eql("changed") 36 | end 37 | end 38 | end 39 | 40 | describe "#result" do 41 | subject { Gisture::Evaluator.new("1+1") } 42 | 43 | it "stores the result returned by evaluating the block" do 44 | subject.evaluate 45 | expect(subject.result).to eql(2) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/gisture/strategies/exec.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | module Strategies 3 | class Exec < Tempfile 4 | attr_reader :executor 5 | 6 | def run!(*args, &block) 7 | log! 8 | 9 | # set args to the default executor array if none were passed and it exists 10 | args = executor if args.empty? && executor.is_a?(Array) 11 | 12 | # map nils to file path in args to allow easily inserting the filepath wherever 13 | # makes sense in your executable arguments (i.e. 'ruby', '-v', nil, '--script-arg') 14 | args.map! { |arg| arg.nil? ? tempfile.path : arg } 15 | 16 | # attempt to apply a default interpreter if nothing was provided 17 | # TODO create a legit map of default interpreter args and apply it 18 | args = ['ruby'] if args.empty? && extname == '.rb' 19 | args = ['node'] if args.empty? && extname == '.js' 20 | 21 | # append the filepath if it was not inserted into the args already 22 | args << tempfile.path unless args.include?(tempfile.path) 23 | 24 | # make file executable if we're just invoking it directly 25 | ::File.chmod(0744, tempfile.path) if args.length == 1 26 | 27 | executed = `#{args.join(' ')}`.strip 28 | block_given? ? yield : executed 29 | end 30 | 31 | protected 32 | 33 | def initialize(content, filename: nil, slug: nil, executor: nil) 34 | super(content, slug: slug, filename: filename) 35 | @executor = executor 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/support/github_api.rb: -------------------------------------------------------------------------------- 1 | require 'github_api/client/gists' 2 | require 'github_api/client/repos' 3 | require 'github_api/client/repos/contents' 4 | 5 | module Github 6 | class Client::Gists < API 7 | def get_with_vcr(*args) 8 | arguments(args, required: [:id]) 9 | 10 | VCR.use_cassette("gists/#{arguments.id}") do 11 | get_without_vcr(*args) 12 | end 13 | end 14 | alias_method :get_without_vcr, :get 15 | alias_method :get, :get_with_vcr 16 | alias_method :find_without_vcr, :find 17 | alias_method :find, :get_with_vcr 18 | 19 | def version_with_vcr(*args) 20 | arguments(args, required: [:id, :version]) 21 | 22 | VCR.use_cassette("gists/#{arguments.id}-#{arguments.version}") do 23 | super 24 | end 25 | end 26 | alias_method :version_without_vcr, :version 27 | alias_method :version, :version_with_vcr 28 | end 29 | 30 | class Client::Repos < API 31 | def get_with_vcr(*args) 32 | arguments(args, required: [:user, :repo]) 33 | 34 | VCR.use_cassette("repos/#{arguments.user}-#{arguments.repo}") do 35 | get_without_vcr(*args) 36 | end 37 | end 38 | alias_method :get_without_vcr, :get 39 | alias_method :get, :get_with_vcr 40 | alias_method :find_without_vcr, :find 41 | alias_method :find, :get_with_vcr 42 | end 43 | 44 | class Client::Repos::Contents < API 45 | def get_with_vcr(*args) 46 | arguments(args, required: [:user, :repo, :path]) 47 | 48 | VCR.use_cassette("repos/#{arguments.user}/#{arguments.repo}/#{arguments.path}") do 49 | get_without_vcr(*args) 50 | end 51 | end 52 | alias_method :get_without_vcr, :get 53 | alias_method :get, :get_with_vcr 54 | alias_method :find_without_vcr, :find 55 | alias_method :find, :get_with_vcr 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /bin/gisture: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'gisture' 5 | require 'commander/import' 6 | 7 | program :name, 'gisture' 8 | program :version, Gisture::VERSION 9 | program :description, 'Run one-off gists or files from the command line' 10 | 11 | command :run do |c| 12 | c.syntax = 'gisture run ' 13 | c.summary = 'Run a gist or file directly from the command line' 14 | c.description = 'Run a github gist or a file or gisture gist from a github repo' 15 | 16 | c.example 'Run a gist using the defaults', 'gisture run markrebec/520b474ea0248d1a0a74' 17 | c.example 'Clone your gist and run it from within the cloned path', 'gisture run markrebec/520b474ea0248d1a0a74 -c' 18 | 19 | c.option '-f', '--filename FILENAME', String, 'Specify a filename if your gist has multiple files. Only applies when executing a github gist' 20 | c.option '-s', '--strategy STRATEGY', String, "Execution strategy to use. Defaults to 'eval'" 21 | c.option '-e', '--evaluator CLASS_NAME', String, "Use a custom evaluator class. Only applies when using the 'eval' strategy" 22 | c.option '-c', '--clone', 'Clone the gist or repo into a local tmp path and run from that working directory' 23 | 24 | c.action do |args, options| 25 | raise ArgumentError, "Please provide a gist or file URI" if args.empty? 26 | 27 | gist_or_repo = Gisture.new(args.first, strategy: options.strategy, filename: options.filename, evaluator: options.evaluator) 28 | 29 | options.clone ? gist_or_repo.clone! : gist_or_repo.destroy_clone! 30 | 31 | result = gist_or_repo.run! 32 | 33 | if options.strategy == 'exec' 34 | puts result 35 | else 36 | result 37 | end 38 | end 39 | end 40 | 41 | command :clone do |c| 42 | c.syntax = 'gisture clone ' 43 | c.summary = 'Clone a github gist or repo into a tempdir' 44 | c.description = 'Clone a github gist or repo into a tempdir directly from the command line' 45 | c.example 'Clone a gist', 'gisture clone markrebec/520b474ea0248d1a0a74' 46 | c.action do |args, options| 47 | raise ArgumentError, "Please provide a gist or file URI" if args.empty? 48 | gist_or_repo = Gisture.new(args.first) 49 | gist_or_repo.clone! 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/gisture/repo_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Gisture::Repo do 4 | context "when passed a valid repo slug" do 5 | subject { Gisture::Repo.new('markrebec/gisture') } 6 | 7 | it "sets the owner and project name correctly" do 8 | end 9 | end 10 | 11 | context "when passed a valid repo URL" do 12 | subject { Gisture::Repo.new('https://github.com/markrebec/gisture') } 13 | 14 | it "sets the owner and project name correctly" do 15 | end 16 | end 17 | 18 | context "when passed an invalid repo" do 19 | it "raises an ArgumentError" do 20 | expect { Gisture::Repo.new('foo') }.to raise_exception(ArgumentError) 21 | expect { Gisture::Repo.new('markrebec/foo/bar') }.to raise_exception(ArgumentError) 22 | expect { Gisture::Repo.new('http://github.com') }.to raise_exception(ArgumentError) 23 | end 24 | end 25 | 26 | context "when whitelisted owners have been configured" do 27 | context "and the repo owner is whitelisted" do 28 | it "does not raise an error" do 29 | begin 30 | Gisture.configure do |config| 31 | config.owners = [:markrebec] 32 | end 33 | 34 | expect { Gisture::Repo.new('markrebec/gisture') }.to_not raise_exception 35 | rescue => e 36 | raise e 37 | ensure 38 | Gisture.configure do |config| 39 | config.owners = nil 40 | end 41 | end 42 | end 43 | end 44 | 45 | context "and the gist owner is not whitelisted" do 46 | it "raises a OwnerBlacklisted error" do 47 | begin 48 | Gisture.configure do |config| 49 | config.owners = [:tester] 50 | end 51 | 52 | expect { Gisture::Repo.new('markrebec/gisture') }.to raise_exception(Gisture::OwnerBlacklisted) 53 | rescue => e 54 | raise e 55 | ensure 56 | Gisture.configure do |config| 57 | config.owners = nil 58 | end 59 | end 60 | end 61 | end 62 | end 63 | 64 | describe "#github" do 65 | subject { Gisture::Repo.new('markrebec/gisture') } 66 | 67 | it "is a github_api client object" do 68 | expect(subject.github).to be_a(Github::Client) 69 | end 70 | end 71 | 72 | # TODO stub out Github::Client::Repos 73 | describe '#repo' do 74 | context "when the repo doesn't exist" do 75 | end 76 | end 77 | 78 | # TODO stub out Github::Client::Repos::Contents 79 | describe '#file' do 80 | context "when the repo doesn't exist" do 81 | end 82 | 83 | context "when the file doesn't exist" do 84 | end 85 | end 86 | 87 | describe '.file' do 88 | context "when the repo doesn't exist" do 89 | end 90 | 91 | context "when the file doesn't exist" do 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/gisture_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Gisture do 4 | describe '.logger' do 5 | context 'when no logger is configured' do 6 | it 'returns an instance of the base logger class' do 7 | expect(Gisture.logger).to be_an_instance_of(Logger) 8 | end 9 | end 10 | 11 | context 'when a logger is configured' do 12 | it 'returns the configured logger' do 13 | begin 14 | Gisture.configure do |config| 15 | config.logger = Logger.new(STDERR) 16 | end 17 | 18 | expect(Gisture.logger).to eql(Gisture.configuration.logger) 19 | rescue => e 20 | raise e 21 | ensure 22 | Gisture.configure do |config| 23 | config.logger = nil 24 | end 25 | end 26 | end 27 | end 28 | end 29 | 30 | describe '.new' do 31 | it 'returns a new Gisture::Gist' do 32 | expect(Gisture.new(TEST_GIST_ID)).to be_a(Gisture::Gist) 33 | end 34 | 35 | context 'with arguments' do 36 | it 'passes the arguments to the gist' do 37 | gist = Gisture.new(TEST_GIST_ID, filename: TEST_GIST_FILENAME, strategy: :require, version: TEST_GIST_VERSION) 38 | expect(gist.filename).to eql(TEST_GIST_FILENAME) 39 | expect(gist.strategy).to eql(:require) 40 | expect(gist.version).to eql(TEST_GIST_VERSION) 41 | end 42 | end 43 | end 44 | 45 | describe '.gist' do 46 | it 'returns a new Gisture::Gist' do 47 | expect(Gisture.gist(TEST_GIST_ID)).to be_a(Gisture::Gist) 48 | end 49 | 50 | context 'with arguments' do 51 | it 'passes the arguments to the gist' do 52 | gist = Gisture.gist(TEST_GIST_ID, filename: TEST_GIST_FILENAME, strategy: :require, version: TEST_GIST_VERSION) 53 | expect(gist.filename).to eql(TEST_GIST_FILENAME) 54 | expect(gist.strategy).to eql(:require) 55 | expect(gist.version).to eql(TEST_GIST_VERSION) 56 | end 57 | end 58 | end 59 | 60 | # TODO should test that the file receives a call to run! 61 | describe '.run' do 62 | it 'creates and runs a new Gisture::Gist' do 63 | expect { Gisture.run(TEST_GIST_ID) }.to_not raise_exception 64 | end 65 | 66 | context 'with arguments' do 67 | it 'passes the arguments to the gist' do 68 | # TODO stub Github::Client::Gists.version 69 | #expect { Gisture.run(TEST_GIST_ID, filename: TEST_GIST_FILENAME, strategy: :require, version: TEST_GIST_VERSION) }.to_not raise_exception 70 | expect { Gisture.run(TEST_GIST_ID, filename: TEST_GIST_FILENAME, strategy: :require) }.to_not raise_exception 71 | end 72 | end 73 | end 74 | 75 | describe '.repo' do 76 | it 'returns a new Gisture::Repo' do 77 | # TODO stub out Github::Client::Repos 78 | #expect(Gisture.repo('markrebec/gisture')).to be_a(Gisture::Repo) 79 | end 80 | end 81 | 82 | describe '.file' do 83 | it 'returns a new Gisture::File' do 84 | # TODO stub out Github::Client::Repos::Contents 85 | #expect(Gisture.file('https://github.com/markrebec/gisture/blob/master/lib/gisture.rb')).to be_a(Gisture::File) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/gisture/repo.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class Repo 3 | include Cloneable 4 | 5 | attr_reader :owner, :name 6 | 7 | class << self 8 | def file(path, strategy: nil) 9 | repo, file = Gisture.parse_file_url(path) 10 | new(repo).file(file, strategy: strategy) 11 | end 12 | 13 | def gists(path) 14 | repo, gists = Gisture.parse_file_url(path) 15 | new(repo).gists(gists) 16 | end 17 | 18 | def gist(path) 19 | gists(path).first 20 | end 21 | 22 | def run!(path, strategy: nil, evaluator: nil, executor: nil, &block) 23 | file(path, strategy: strategy, evaluator: evaluator, executor: executor).run!(&block) 24 | end 25 | end 26 | 27 | def github 28 | @github ||= Github.new(Gisture.configuration.github.to_h) 29 | end 30 | 31 | def repo 32 | @repo ||= github.repos.get user: owner, repo: name 33 | end 34 | 35 | def file(path, strategy: nil, evaluator: nil, executor: nil) 36 | if cloned? 37 | Gisture::File::Cloned.new(clone_path, path, slug: "#{owner}/#{name}", strategy: strategy, evaluator: evaluator, executor: executor) 38 | else 39 | file = github.repos.contents.get(user: owner, repo: name, path: path).body 40 | Gisture::Repo::File.new(file, slug: "#{owner}/#{name}", root: clone_path, strategy: strategy, evaluator: evaluator, executor: executor) 41 | end 42 | end 43 | 44 | def files(path) 45 | if cloned? 46 | Dir[::File.join(clone_path, path, '*')].map { |f| Hashie::Mash.new({name: ::File.basename(f), path: ::File.join(path, ::File.basename(f))}) } 47 | else 48 | github.repos.contents.get(user: owner, repo: name, path: path).body 49 | end 50 | end 51 | 52 | def gists(path) 53 | if ::File.basename(path).match(Gisture::GISTURE_FILE_REGEX) 54 | Gisture::Repo::Gists.new([Gisture::Repo::Gist.load(self, path)]) 55 | else # must be a directory, so let's look for gists 56 | Gisture::Repo::Gists.new(files(path).select { |f| f.name.match(Gisture::GISTURE_FILE_REGEX) }.map { |f| Gisture::Repo::Gist.load(self, f.path) }) 57 | end 58 | end 59 | 60 | def gist(path) 61 | gists(path).first 62 | end 63 | 64 | def run!(path, *args, strategy: nil, evaluator: nil, executor: nil, &block) 65 | # best guess that it's a gisture file or a directory, otherwise try a file 66 | if ::File.basename(path).match(Gisture::GISTURE_FILE_REGEX) || ::File.extname(path).empty? 67 | gists(path).run!(*args, &block) 68 | else 69 | file(path, strategy: strategy, evaluator: evaluator, executor: executor).run!(*args, &block) 70 | end 71 | rescue => e 72 | Gisture.logger.error "[gisture] #{e.class.name}: #{e.message}\n\t[gisture] #{e.backtrace.join("\n\t[gisture] ")}" 73 | raise AmbiguousRepoFile, "Don't know how to run '#{path}', try running it as a gist or a file specifically" 74 | end 75 | alias_method :run, :run! 76 | 77 | def clone_url 78 | "https://#{Gisture.configuration.github.auth_str}@github.com/#{owner}/#{name}.git" 79 | end 80 | 81 | protected 82 | 83 | def initialize(repo) 84 | @owner, @name = Gisture.parse_repo_url(repo) 85 | raise OwnerBlacklisted.new(owner) unless Gisture.configuration.whitelisted?(owner) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/gisture/gist.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class Gist 3 | include Cloneable 4 | 5 | attr_reader :evaluator, :executor, :filename, :gist_id, :strategy, :version 6 | alias_method :name, :gist_id 7 | 8 | def self.run!(gist, *args, strategy: nil, filename: nil, version: nil, evaluator: nil, executor: nil, &block) 9 | new(gist, strategy: strategy, filename: filename, version: version, evaluator: nil, executor: nil).run!(*args, &block) 10 | end 11 | 12 | def run!(*args, &block) 13 | file.run! *args, &block 14 | end 15 | 16 | def require!(*args, &block) 17 | file.require! *args, &block 18 | end 19 | 20 | def load!(*args, &block) 21 | file.load! *args, &block 22 | end 23 | 24 | def eval!(*args, &block) 25 | file.eval! *args, &block 26 | end 27 | 28 | def exec!(*args, &block) 29 | file.exec! *args, &block 30 | end 31 | 32 | def github 33 | @github ||= Github.new(Gisture.configuration.github.to_h) 34 | end 35 | 36 | def gist 37 | @gist ||= begin 38 | if @version.nil? 39 | g = github.gists.get(gist_id) 40 | else 41 | g = github.gists.version(gist_id, @version) 42 | end 43 | raise OwnerBlacklisted.new(g.owner.login) unless Gisture.configuration.whitelisted?(g.owner.login) 44 | g 45 | end 46 | end 47 | 48 | def file_exists?(fname) 49 | !gist.files[fname].nil? 50 | end 51 | 52 | def file(fname=nil) 53 | fname ||= (filename || gist.files.first[1].filename) 54 | raise ArgumentError, "The filename '#{fname}' was not found in the list of files for the gist '#{gist_id}'" unless file_exists?(fname) 55 | 56 | if cloned? 57 | Gisture::File::Cloned.new(clone_path, fname, slug: "#{owner}/#{gist_id}", strategy: strategy, evaluator: evaluator, executor: executor) 58 | else 59 | Gisture::File.new(gist.files[fname], slug: "#{owner}/#{gist_id}", strategy: strategy, evaluator: evaluator, executor: executor) 60 | end 61 | end 62 | 63 | def owner 64 | gist.owner.login 65 | end 66 | 67 | def clone_url 68 | @clone_url ||= "https://#{Gisture.configuration.github.auth_str}@gist.github.com/#{gist_id}.git" 69 | end 70 | 71 | def strategy=(strat) 72 | strat_key = strat 73 | strat_key = strat.keys.first if strat.respond_to?(:keys) 74 | raise ArgumentError, "Invalid strategy '#{strat_key}'. Must be one of #{File::STRATEGIES.join(', ')}" unless File::STRATEGIES.include?(strat_key.to_sym) 75 | @strategy = strat 76 | end 77 | 78 | def to_h 79 | { gist_id: gist_id, 80 | version: version, 81 | strategy: strategy, 82 | filename: filename, 83 | evaluator: evaluator, 84 | executor: executor } 85 | end 86 | 87 | protected 88 | 89 | def initialize(gist, strategy: nil, filename: nil, version: nil, evaluator: nil, executor: nil) 90 | gist_id, gist_version = Gisture.parse_gist_url(gist) 91 | @gist_id = gist_id 92 | @version = (version || gist_version) 93 | @filename = filename 94 | @evaluator = evaluator 95 | @executor = executor 96 | self.strategy = strategy || Gisture.configuration.strategy 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/gisture/file.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class File 3 | attr_reader :evaluator, :executor, :file, :slug, :root, :localized, :strategy 4 | 5 | STRATEGIES = [:eval, :exec, :load, :require] 6 | 7 | def run!(*args, &block) 8 | strat_key = strategy 9 | if strategy.respond_to?(:keys) 10 | strat_key = strategy.keys.first 11 | if strategy[strat_key].is_a?(Array) 12 | args = args.concat(strategy[strat_key]) 13 | else 14 | args << strategy[strat_key] 15 | end 16 | end 17 | 18 | send "#{strat_key}!".to_sym, *args, &block 19 | end 20 | 21 | def relative_path 22 | file.path || file.filename 23 | end 24 | 25 | def run_args 26 | # important to use @localized here so it's nil if we haven't intentionally localized 27 | @run_args ||= {filename: relative_path, slug: slug, tempfile: @localized} 28 | end 29 | 30 | # TODO rework these to use .run_from!(root) if file is cloned/localized 31 | def eval!(*args, &block) 32 | Strategies::Eval.new(content, run_args.except(:tempfile)).run!(*args, &block) 33 | end 34 | 35 | def exec!(*args, &block) 36 | Strategies::Exec.new(content, run_args).run!(*args, &block) 37 | end 38 | 39 | def require!(*args, &block) 40 | Strategies::Require.new(content, run_args).run!(*args, &block) 41 | end 42 | 43 | def load!(*args, &block) 44 | Strategies::Load.new(content, run_args).run!(*args, &block) 45 | end 46 | 47 | def strategy=(strat) 48 | strat_key = strat 49 | strat_key = strat.keys.first if strat.respond_to?(:keys) 50 | raise ArgumentError, "Invalid strategy '#{strat_key}'. Must be one of #{STRATEGIES.join(', ')}" unless STRATEGIES.include?(strat_key.to_sym) 51 | @strategy = strat 52 | end 53 | 54 | def localized_path 55 | ::File.join(root, (file.path || file.filename)) 56 | rescue => e 57 | nil.to_s 58 | end 59 | 60 | def exists_locally? 61 | ::File.exists?(localized_path) 62 | end 63 | 64 | def localized? 65 | exists_locally? && localize 66 | end 67 | 68 | def localize 69 | if localized? 70 | @localized ||= ::File.new(localized_path) 71 | else 72 | localize! 73 | end 74 | end 75 | 76 | def localize! 77 | raise FileLocalizationError, "Cannot localize without a :root path" if root.nil? 78 | @localized = begin 79 | Gisture.logger.info "[gisture] Localizing #{file.path || file.filename} into #{root}" 80 | FileUtils.mkdir_p ::File.dirname(localized_path) 81 | localize_file! 82 | end 83 | end 84 | 85 | def localize_file! 86 | local_file = ::File.open(localized_path, 'w') 87 | local_file.write(file.content) 88 | local_file.close 89 | local_file 90 | end 91 | 92 | def delocalize! 93 | FileUtils.rm_f localized_path 94 | @localized = nil 95 | end 96 | alias_method :unlink!, :delocalize! 97 | 98 | def extname 99 | @extname ||= ::File.extname(file.filename) 100 | end 101 | alias_method :extension, :extname 102 | 103 | def method_missing(meth, *args, &block) 104 | return file.send(meth, *args, &block) if file.respond_to?(meth) 105 | super 106 | end 107 | 108 | def respond_to_missing?(meth, include_private=false) 109 | file.respond_to?(meth, include_private) 110 | end 111 | 112 | protected 113 | 114 | def initialize(file, slug: nil, root: nil, strategy: nil, evaluator: nil, executor: nil) 115 | @file = file 116 | @slug = slug 117 | @root = root || ::File.join(Gisture.configuration.tmpdir, slug) 118 | @evaluator = evaluator || Gisture::Evaluator 119 | @executor = executor 120 | self.strategy = strategy || Gisture.configuration.strategy 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/gisture/repo/gist.rb: -------------------------------------------------------------------------------- 1 | module Gisture 2 | class Repo::Gist 3 | attr_reader :repo, :gist 4 | 5 | def self.load(repo, path) 6 | hash = YAML.load(repo.file(path).content).symbolize_keys 7 | hash[:name] ||= path 8 | new(repo, hash) 9 | end 10 | 11 | def multi? 12 | gist.key?(:gistures) || gist.key?(:gists) 13 | end 14 | 15 | def gist_hashes 16 | # can be a hash of hashes or an array of hashes 17 | hashes = (gist[:gistures] || gist[:gists] || []) 18 | if hashes.is_a?(Array) 19 | hashes = hashes.each_with_index.map { |h,i| {name: "#{gist[:name]}:#{i+1}"}.merge(h) } 20 | else 21 | hashes = hashes.map { |k,v| {name: "#{gist[:name]}:#{k}"}.merge(v) } 22 | end 23 | hashes 24 | end 25 | 26 | def gists 27 | @gists ||= Repo::Gists.new(gist_hashes.map { |mg| self.class.new(repo, mg) }) 28 | end 29 | 30 | def file(file_path, strategy: nil) 31 | repo.file(file_path, strategy: strategy) 32 | end 33 | 34 | def runnable 35 | @runnable ||= file(gist[:path], strategy: gist[:strategy]) 36 | end 37 | 38 | def resources 39 | @resources ||= (gist[:resources] || []).map { |r| file(r) } 40 | end 41 | 42 | def clone? 43 | gist[:clone] == true || !resources.empty? 44 | end 45 | 46 | def cloned? 47 | runnable.localized? && resources.all?(&:localized?) 48 | end 49 | 50 | def clone_path 51 | repo.clone_path 52 | end 53 | 54 | def clone! 55 | resources.each do |resource| 56 | resource.localize! 57 | end 58 | 59 | runnable.localize! 60 | end 61 | 62 | def clone 63 | resources.each do |resource| 64 | resource.localize 65 | end 66 | 67 | runnable.localize 68 | end 69 | 70 | def destroy_cloned_files! 71 | return false if repo.cloned? 72 | resources.each do |resource| 73 | resource.delocalize! 74 | end 75 | 76 | runnable.delocalize! 77 | end 78 | alias_method :destroy_clone!, :destroy_cloned_files! 79 | 80 | def run!(*args, &block) 81 | if multi? 82 | run_gists!(*args, &block) 83 | else 84 | run_gist!(*args, &block) 85 | end 86 | end 87 | 88 | def run_gists!(*args, &block) 89 | Gisture.logger.info "[gisture] Found multi-gist #{gist.name} with #{gists.count} gists" 90 | gists.run!(*args, &block) 91 | end 92 | 93 | def run_gist!(*args, &block) 94 | Gisture.logger.info "[gisture] Found gist #{gist.name} from #{::File.join(repo.owner, repo.name)}" 95 | if clone? 96 | clone_and_run!(*args, &block) 97 | else 98 | run_with_options!(*args, &block) 99 | end 100 | end 101 | 102 | def clone_and_run!(*args, &block) 103 | clone! 104 | chdir_and_run!(clone_path, *args, &block) 105 | end 106 | 107 | def chdir_and_run!(path, *args, &block) 108 | Dir.chdir(path) { run_with_options!(*args, &block) } 109 | end 110 | 111 | def run_with_options!(*args, &block) 112 | runnable.run!(*run_options.concat(args), &block) 113 | end 114 | 115 | def evaluator 116 | @evaluator ||= eval(gist[:evaluator]) if (!gist.key?(:strategy) || gist[:strategy].to_sym == :eval) && gist.key?(:evaluator) 117 | end 118 | 119 | def executor 120 | @executor ||= gist[:executor] if (gist.key?(:strategy) && gist[:strategy].to_sym == :exec) && gist.key?(:executor) 121 | end 122 | 123 | def run_options 124 | run_opts = [] 125 | run_opts << evaluator unless evaluator.nil? 126 | run_opts = executor unless executor.nil? 127 | run_opts 128 | end 129 | 130 | protected 131 | 132 | def initialize(repo, gist) 133 | @repo = repo 134 | @gist = Hashie::Mash.new(gist) 135 | @gist[:name] ||= @gist[:path] 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'gisture' 2 | require 'rspec' 3 | require 'vcr' 4 | require 'simplecov' 5 | require 'coveralls' 6 | Coveralls.wear! 7 | #SimpleCov.formatter = Coveralls::SimpleCov::Formatter 8 | #SimpleCov.start do 9 | # add_filter '/spec' 10 | #end 11 | 12 | Dir[File.join(File.dirname(__FILE__), '..', "spec/support/**/*.rb")].each { |f| require f } 13 | 14 | Gisture.configure do |config| 15 | config.github.oauth_token = ENV['GITHUB_OAUTH_TOKEN'] 16 | end 17 | 18 | VCR.configure do |config| 19 | config.cassette_library_dir = "spec/cassettes" 20 | config.hook_into :webmock 21 | end 22 | 23 | TEST_GIST_ID = "520b474ea0248d1a0a74" 24 | TEST_GIST_URL = "https://gist.github.com/markrebec/520b474ea0248d1a0a74" 25 | TEST_GIST_VERSION = "49a9d887eeb8c723ab23deddfbbb75d4b70e8014" 26 | TEST_GIST_VERSION_URL = "https://gist.github.com/markrebec/520b474ea0248d1a0a74/49a9d887eeb8c723ab23deddfbbb75d4b70e8014" 27 | TEST_GIST_FILENAME = "test.rb" 28 | MULTI_FILE_TEST_GIST_ID = "0417bf78a7c2b825b4ef" 29 | MULTI_FILE_TEST_GIST_FILENAMES = ['file_one.rb', 'file_two.rb'] 30 | 31 | RSpec.configure do |config| 32 | #config.before(:suite) do 33 | # ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:" 34 | # capture_stdout { load "db/schema.rb" } 35 | # load 'support/models.rb' 36 | #end 37 | 38 | # rspec-expectations config goes here. You can use an alternate 39 | # assertion/expectation library such as wrong or the stdlib/minitest 40 | # assertions if you prefer. 41 | config.expect_with :rspec do |expectations| 42 | # This option will default to `true` in RSpec 4. It makes the `description` 43 | # and `failure_message` of custom matchers include text for helper methods 44 | # defined using `chain`, e.g.: 45 | # be_bigger_than(2).and_smaller_than(4).description 46 | # # => "be bigger than 2 and smaller than 4" 47 | # ...rather than: 48 | # # => "be bigger than 2" 49 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 50 | end 51 | 52 | # rspec-mocks config goes here. You can use an alternate test double 53 | # library (such as bogus or mocha) by changing the `mock_with` option here. 54 | config.mock_with :rspec do |mocks| 55 | # Prevents you from mocking or stubbing a method that does not exist on 56 | # a real object. This is generally recommended, and will default to 57 | # `true` in RSpec 4. 58 | mocks.verify_partial_doubles = true 59 | end 60 | 61 | # Many RSpec users commonly either run the entire suite or an individual 62 | # file, and it's useful to allow more verbose output when running an 63 | # individual spec file. 64 | if config.files_to_run.one? 65 | # Use the documentation formatter for detailed output, 66 | # unless a formatter has already been configured 67 | # (e.g. via a command-line flag). 68 | config.default_formatter = 'doc' 69 | end 70 | 71 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 72 | # For more details, see: 73 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 74 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 75 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 76 | config.disable_monkey_patching! 77 | 78 | # Run specs in random order to surface order dependencies. If you find an 79 | # order dependency and want to debug it, you can fix the order by providing 80 | # the seed, which is printed after each run. 81 | # --seed 1234 82 | config.order = :random 83 | 84 | # Seed global randomization in this process using the `--seed` CLI option. 85 | # Setting this allows you to use `--seed` to deterministically reproduce 86 | # test failures related to randomization by passing the same `--seed` value 87 | # as the one that triggered the failure. 88 | Kernel.srand config.seed 89 | 90 | # Print the 10 slowest examples and example groups at the 91 | # end of the spec run, to help surface which specs are running 92 | # particularly slow. 93 | #config.profile_examples = 10 94 | 95 | # These two settings work together to allow you to limit a spec run 96 | # to individual examples or groups you care about by tagging them with 97 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 98 | # get run. 99 | #config.filter_run :focus 100 | #config.run_all_when_everything_filtered = true 101 | end 102 | 103 | # TODO look into why I need to patch these to work with default behavior? 104 | class FalseClass 105 | def false? 106 | true 107 | end 108 | 109 | def true? 110 | false 111 | end 112 | end 113 | 114 | class TrueClass 115 | def false? 116 | false 117 | end 118 | 119 | def true? 120 | true 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/gisture.rb: -------------------------------------------------------------------------------- 1 | require 'canfig' 2 | require 'git' 3 | require 'github_api' 4 | require 'gisture/github_api/client/gists' 5 | require 'gisture/version' 6 | require 'gisture/errors' 7 | require 'gisture/evaluator' 8 | require 'gisture/cloneable' 9 | require 'gisture/strategies' 10 | require 'gisture/file' 11 | require 'gisture/file/cloned' 12 | require 'gisture/gist' 13 | require 'gisture/repo' 14 | require 'gisture/repo/file' 15 | require 'gisture/repo/gists' 16 | require 'gisture/repo/gist' 17 | require 'gisture/railtie' if defined?(Rails) 18 | 19 | module Gisture 20 | GIST_VERSION_URL_REGEX = /\Ahttp.+([0-9a-f]{20,20})\/([0-9a-f]{40,40})\/?\Z/ 21 | GIST_URL_REGEX = /\Ahttp.+([0-9a-f]{20,20})\/?\Z/ 22 | GIST_PATH_REGEX = /\A[a-z0-9_\-]+\/([0-9a-f]{20,20})\/?\Z/ 23 | GIST_ID_REGEX = /\A([0-9a-f]{20,20})\Z/ 24 | FILE_URL_REGEX = /\A((http[s]?:\/\/)?github\.com\/)?(([a-z0-9_\-\.]+)\/([a-z0-9_\-\.]+))(\/[a-z0-9_\-\.\/]+)\Z/i 25 | REPO_URL_REGEX = /\A((http[s]?:\/\/)?github\.com\/)?([a-z0-9_\-\.]+)\/([a-z0-9_\-\.]+)\/?\Z/i 26 | GISTURE_FILE_REGEX = /\A(gisture\.ya?ml|.+\.gist|.+\.gisture)\Z/ # gisture.yml, gisture.yaml, whatever.gist, whatever.gisture 27 | 28 | include Canfig::Module 29 | 30 | configure do |config| 31 | config.logger = nil # defaults to STDOUT but will use Rails.logger in a rails environment 32 | config.strategy = :eval # default execution strategy 33 | config.tmpdir = Dir.tmpdir # location to store gist tempfiles 34 | config.owners = nil # only allow gists/repos/etc. from whitelisted owners (str/sym/arr) 35 | 36 | # config options for the github_api gem 37 | config.github = Canfig::OpenConfig.new do |github_config| 38 | github_config.basic_auth = nil # user:password string 39 | github_config.oauth_token = nil # oauth authorization token 40 | github_config.client_id = nil # oauth client id 41 | github_config.client_secret = nil # oauth client secret 42 | github_config.user = nil # global user used in requets if none provided 43 | github_config.org = nil # global organization used in request if none provided 44 | 45 | def auth_str 46 | return "#{oauth_token}:x-oauth-basic" if oauth_token 47 | return basic_auth if basic_auth 48 | end 49 | end 50 | 51 | def whitelisted?(owner) 52 | owners.nil? || owners.empty? || [owners].flatten.map(&:to_s).include?(owner) 53 | end 54 | end 55 | 56 | def self.logger 57 | configuration.logger || Logger.new(STDOUT) 58 | end 59 | 60 | def self.new(url, strategy: nil, filename: nil, version: nil, evaluator: nil, executor: nil) 61 | case url.to_s 62 | when GIST_VERSION_URL_REGEX, GIST_URL_REGEX, GIST_PATH_REGEX, GIST_ID_REGEX 63 | Gisture::Gist.new(url, strategy: strategy, filename: filename, version: version, evaluator: evaluator, executor: executor) 64 | when FILE_URL_REGEX 65 | if ::File.basename(url).match(GISTURE_FILE_REGEX) 66 | Gisture::Repo.gist(url) 67 | elsif ::File.extname(url).empty? 68 | Gisture::Repo.gists(url) 69 | else 70 | Gisture::Repo.file(url, strategy: strategy, evaluator: evaluator, executor: executor) 71 | end 72 | when REPO_URL_REGEX 73 | Gisture::Repo.new(url) 74 | else 75 | raise ArgumentError, "Invalid argument: #{url} does not appear to be a valid gist, repo or file." 76 | end 77 | end 78 | 79 | def self.run(url, *args, strategy: nil, filename: nil, version: nil, evaluator: nil, executor: nil, &block) 80 | new(url, strategy: strategy, filename: filename, version: version, evaluator: evaluator, executor: executor).run!(*args, &block) 81 | end 82 | 83 | def self.gist(gist, strategy: nil, filename: nil, version: nil, evaluator: nil, executor: nil) 84 | Gisture::Gist.new(gist, strategy: strategy, filename: filename, version: version, evaluator: evaluator, executor: executor) 85 | end 86 | 87 | def self.repo(repo) 88 | Gisture::Repo.new(repo) 89 | end 90 | 91 | def self.file(path, strategy: nil, evaluator: nil, executor: nil) 92 | Gisture::Repo.file(path, strategy: strategy, evaluator: evaluator, executor: executor) 93 | end 94 | 95 | def self.gists(path) 96 | Gisture::Repo.gists(path) 97 | end 98 | 99 | def self.invalid_url!(url) 100 | raise ArgumentError, "Invalid argument: #{url} does not appear to be a valid gist, repo or file." 101 | end 102 | 103 | def self.parse_repo_url(repo_url) 104 | repo = repo_url.match(REPO_URL_REGEX) 105 | invalid_url!(repo_url) if repo.nil? 106 | [repo[3], repo[4]] 107 | end 108 | 109 | def self.parse_file_url(file_url) 110 | file = file_url.match(FILE_URL_REGEX) 111 | invalid_url!(file_url) if file.nil? 112 | [file[3], file[6]] 113 | end 114 | 115 | def self.parse_gist_url(gist_url) 116 | case gist_url.to_s 117 | when GIST_VERSION_URL_REGEX 118 | matches = gist_url.match(GIST_VERSION_URL_REGEX) 119 | [matches[1], matches[2]] 120 | when GIST_URL_REGEX 121 | [gist_url.match(GIST_URL_REGEX)[1], nil] 122 | when GIST_PATH_REGEX 123 | [gist_url.match(GIST_PATH_REGEX)[1], nil] 124 | when GIST_ID_REGEX 125 | [gist_url.match(GIST_ID_REGEX)[1], nil] 126 | else 127 | invalid_url!(gist_url) 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /spec/gisture/gist_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Gisture::Gist do 4 | context "when passing a gist ID" do 5 | it "sets the gist ID as the gist_id" do 6 | expect(Gisture::Gist.new(TEST_GIST_ID).gist_id).to eql(TEST_GIST_ID) 7 | end 8 | 9 | context "when passing a version" do 10 | it "sets the version" do 11 | expect(Gisture::Gist.new(TEST_GIST_ID, version: TEST_GIST_VERSION).version).to eql(TEST_GIST_VERSION) 12 | end 13 | end 14 | end 15 | 16 | context "when passing a gist URL" do 17 | context "without a version" do 18 | it "extracts and sets the gist ID as the gist_id" do 19 | expect(Gisture::Gist.new(TEST_GIST_URL).gist_id).to eql(TEST_GIST_ID) 20 | end 21 | 22 | context "when passing a version separately" do 23 | it "sets the version" do 24 | expect(Gisture::Gist.new(TEST_GIST_URL, version: TEST_GIST_VERSION).version).to eql(TEST_GIST_VERSION) 25 | end 26 | end 27 | end 28 | 29 | context "with a version" do 30 | it "extracts and sets the gist ID as the gist_id" do 31 | expect(Gisture::Gist.new(TEST_GIST_VERSION_URL).gist_id).to eql(TEST_GIST_ID) 32 | end 33 | 34 | it "extracts and sets the gist version as the version" do 35 | expect(Gisture::Gist.new(TEST_GIST_VERSION_URL).version).to eql(TEST_GIST_VERSION) 36 | end 37 | 38 | context "when passing a version separately" do 39 | it "overrides the version with the one explicitly provided" do 40 | expect(Gisture::Gist.new(TEST_GIST_VERSION_URL, version: "abc123").version).to eql("abc123") 41 | end 42 | end 43 | end 44 | end 45 | 46 | context "when passing a filename" do 47 | it "sets the filename" do 48 | expect(Gisture::Gist.new(TEST_GIST_ID, filename: TEST_GIST_FILENAME).filename).to eql(TEST_GIST_FILENAME) 49 | end 50 | end 51 | 52 | context "when passing a strategy" do 53 | context "which is a valid strategy" do 54 | it "sets the requested strategy" do 55 | expect(Gisture::Gist.new(TEST_GIST_ID, strategy: :load).strategy).to eql(:load) 56 | end 57 | end 58 | 59 | context "which is not a valid strategy" do 60 | it "raises an ArgumentError" do 61 | expect { Gisture::Gist.new(TEST_GIST_ID, strategy: :foo) }.to raise_exception(ArgumentError) 62 | end 63 | end 64 | end 65 | 66 | context "when not passing a strategy" do 67 | it "uses the default configured strategy" do 68 | begin 69 | Gisture.configure do |config| 70 | config.strategy = :load 71 | end 72 | 73 | expect(Gisture::Gist.new(TEST_GIST_ID).strategy).to eql(:load) 74 | rescue => e 75 | raise e 76 | ensure 77 | Gisture.configure do |config| 78 | config.strategy = :eval 79 | end 80 | end 81 | end 82 | end 83 | 84 | describe "#github" do 85 | subject { Gisture::Gist.new(TEST_GIST_ID) } 86 | 87 | it "is a github_api client object" do 88 | expect(subject.github).to be_a(Github::Client) 89 | end 90 | end 91 | 92 | describe "#gist" do 93 | subject { Gisture::Gist.new(TEST_GIST_ID) } 94 | 95 | it "is the gist that was requested" do 96 | expect(subject.gist.id).to eql(TEST_GIST_ID) 97 | end 98 | 99 | context "when whitelisted owners have been configured" do 100 | context "and the gist owner is whitelisted" do 101 | it "does not raise an error" do 102 | begin 103 | Gisture.configure do |config| 104 | config.owners = [:markrebec] 105 | end 106 | 107 | expect { Gisture::Gist.new(TEST_GIST_ID).gist }.to_not raise_exception 108 | rescue => e 109 | raise e 110 | ensure 111 | Gisture.configure do |config| 112 | config.owners = nil 113 | end 114 | end 115 | end 116 | end 117 | 118 | context "and the gist owner is not whitelisted" do 119 | it "raises a OwnerBlacklisted error" do 120 | begin 121 | Gisture.configure do |config| 122 | config.owners = [:tester] 123 | end 124 | 125 | expect { Gisture::Gist.new(TEST_GIST_ID).gist }.to raise_exception(Gisture::OwnerBlacklisted) 126 | rescue => e 127 | raise e 128 | ensure 129 | Gisture.configure do |config| 130 | config.owners = nil 131 | end 132 | end 133 | end 134 | end 135 | end 136 | end 137 | 138 | describe "#file" do 139 | it "returns a Gisture::File" do 140 | expect(Gisture::Gist.new(TEST_GIST_ID).file).to be_a(Gisture::File) 141 | end 142 | 143 | context "when a gist contains a single file" do 144 | it "returns the file" do 145 | expect(Gisture::Gist.new(TEST_GIST_ID).file.filename).to eql(TEST_GIST_FILENAME) 146 | end 147 | end 148 | 149 | context "when a gist contains more than one file" do 150 | context "and a filename is present" do 151 | subject { Gisture::Gist.new(MULTI_FILE_TEST_GIST_ID, filename: MULTI_FILE_TEST_GIST_FILENAMES.sample) } 152 | 153 | it "returns the specified file" do 154 | expect(subject.file.filename).to eql(subject.filename) 155 | end 156 | end 157 | 158 | context "and no filename is present" do 159 | subject { Gisture::Gist.new(MULTI_FILE_TEST_GIST_ID) } 160 | 161 | it "uses the first file in the gist" do 162 | expect(subject.file.filename).to eql(MULTI_FILE_TEST_GIST_FILENAMES.first) 163 | end 164 | end 165 | end 166 | end 167 | 168 | describe "#strategy=" do 169 | subject { Gisture::Gist.new(TEST_GIST_ID) } 170 | 171 | context "when passed a valid strategy" do 172 | it "sets the strategy" do 173 | subject.strategy = :load 174 | expect(subject.strategy).to eql(:load) 175 | end 176 | end 177 | 178 | context "when passed an invalid strategy" do 179 | it "raises an ArgumentError" do 180 | expect { subject.strategy = :foo }.to raise_exception(ArgumentError) 181 | end 182 | end 183 | end 184 | 185 | describe "#to_h" do 186 | subject { Gisture::Gist.new(TEST_GIST_ID, filename: "test.rb") } 187 | 188 | it "returns a hash of the gist attributes" do 189 | expect(subject.to_h).to eql({gist_id: TEST_GIST_ID, strategy: :eval, filename: "test.rb", version: nil, evaluator: nil, executor: nil}) 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gisture 2 | 3 | [![Build Status](https://travis-ci.org/markrebec/gisture.png)](https://travis-ci.org/markrebec/gisture) 4 | [![Coverage Status](https://coveralls.io/repos/markrebec/gisture/badge.svg?1=1)](https://coveralls.io/r/markrebec/gisture) 5 | [![Code Climate](https://codeclimate.com/github/markrebec/gisture.png)](https://codeclimate.com/github/markrebec/gisture) 6 | [![Gem Version](https://badge.fury.io/rb/gisture.png?1=1)](http://badge.fury.io/rb/gisture) 7 | [![Dependency Status](https://gemnasium.com/markrebec/gisture.png)](https://gemnasium.com/markrebec/gisture) 8 | 9 | Execute one-off gists inline or in the background. 10 | 11 | ```ruby 12 | Gisture.run('c3b478ef0592eacad361') 13 | ``` 14 | 15 | For convenience, you can also use gist.github.com URLs, with or without revision information in them. 16 | 17 | ```ruby 18 | Gisture.run('https://gist.github.com/markrebec/c3b478ef0592eacad361') 19 | Gisture.run('https://gist.github.com/markrebec/c3b478ef0592eacad361/7714df11a3babaa78f27027844ac2f0c1a8348c1') 20 | ``` 21 | 22 | The above will run [this gist](https://gist.github.com/markrebec/c3b478ef0592eacad361) and print "You are using Gisture version VERSION" (whatever version of gisture you're using) 23 | 24 | Don't uses gists, but have a public or private github repo where you store snippets, scripts, etc.? You can also use gisture with github repos. 25 | 26 | ```ruby 27 | Gisture.file('markrebec/gisture/lib/gisture/version.rb').run! 28 | Gisture.repo('markrebec/gisture').file('lib/gisture/version.rb').run! 29 | ``` 30 | 31 | **Note:** I'm still fleshing out what the final version of the API will look like. I'll be making every effort to keep things backwards compatible while working towards a 1.0 release, but you should expect some things to change. 32 | 33 | ## Getting Started 34 | 35 | Add the gem to your project's Gemfile: 36 | 37 | gem 'gisture' 38 | 39 | Then run `bundle install`. Or install gisture system wide with `gem install gisture`. 40 | 41 | ### Configuration 42 | 43 | Gisture uses the [github_api](http://peter-murach.github.io/github/) gem to load gists, and passes through a subset of configuration options, mostly around authentication. You can configure these options and all other gisture options using `Gisture.configure` with a block wherever is appropriate for your project (for example in a rails config initializer). Below is an example along with list of all gisture config options and their defaults. 44 | 45 | ```ruby 46 | Gisture.configure do |config| 47 | # config options for the github_api gem 48 | config.github.basic_auth = nil # user:password string 49 | config.github.oauth_token = nil # oauth authorization token 50 | config.github.client_id = nil # oauth client id 51 | config.github.client_secret = nil # oauth client secret 52 | config.github.user = nil # global user used in requets if none provided 53 | config.github.org = nil # global organization used in request if none provided 54 | 55 | config.logger = nil # defaults to STDOUT but will use Rails.logger in a rails environment 56 | config.strategy = :eval # default execution strategy 57 | config.tmpdir = Dir.tmpdir # location to store gist tempfiles when using the require or load strategies 58 | config.owners = nil # only allow gists/repos/etc. from whitelisted owners (str/sym/arr) 59 | end 60 | ``` 61 | 62 | Most of the options are self explanatory or easily explained in their associated comments above. The one that's worth elaborating on is `config.owners`. Setting this option to a symbol, string or array enables whitelisting by owner(s) and will force gisture to only allow running gists or files from repos owned by these whitelisted owners. If you enable whitelisting and try to run a gist or file owned by a user/org not included in the whitelist, gisture will raise an exception and let you know. 63 | 64 | ## Usage 65 | 66 | ### Gists 67 | There are a couple ways to load and run a gist. You can use the `Gisture::Gist` class directly, or use shortcut methods directly on the `Gisture` module. The only required argument is the ID of the gist with which you'd like to work. 68 | 69 | ```ruby 70 | Gisture.run(gist_id) 71 | Gisture.new(gist_id).run! 72 | gist = Gisture::Gist.new(gist_id) 73 | gist.run! 74 | ``` 75 | 76 | You can also pass an execution strategy (outlined further below), optional filename (if your gist contains more than one file) and an optional version if you want to load a previous commit from the gist's history. 77 | 78 | ```ruby 79 | Gisture.run(gist_id, strategy: :require, filename: 'my_file.rb', version: 'abc123') 80 | gist = Gisture::Gist.new(gist_id, strategy: :require, filename: 'my_file.rb', version: 'abc123') 81 | gist.run! 82 | ``` 83 | 84 | #### Multiple Files 85 | 86 | If your gist contains more than one file, you can provide the optional `filename` parameter to specify which file you'd like to run. **If you do not specify a `filename` argument, the first file in your gist will be chosen automatically.** 87 | 88 | ### Repositories & Files 89 | 90 | Gisture doesn't only run gists. It also allows you to one-off include/execute any file within a github repository the same way you can a gist. The syntax is similar, and all the same logic outlined below regarding callbacks, execution strategies, etc. applies to these files as well as gists. 91 | 92 | ```ruby 93 | file = Gisture.file('markrebec/gisture/lib/gisture/version.rb') 94 | file.run! 95 | repo = Gisture::Repo.new('markrebec/gisture') 96 | repo.file('lib/gisture/version.rb').run! 97 | Gisture.repo('markrebec/gisture').file('lib/gisture/version.rb').run! 98 | ``` 99 | 100 | Like gists, you can pass an execution strategy when running a file from a repo. 101 | 102 | ```ruby 103 | Gisture.file('markrebec/gisture/lib/gisture/version.rb', strategy: :require).run! 104 | Gisture.repo('markrebec/gisture').file('lib/gisture/version.rb', strategy: :require).run! 105 | ``` 106 | 107 | ### Callbacks 108 | 109 | You can provide an optional callback block to execute after your gist has been loaded and run. This can be handy if, for example, your gist defines a few classes and/or methods, but does not do anything with them directly or perform any inline functionality. In that case, you might want to make calls to those methods or classes after your gist has been loaded to perform some action. 110 | 111 | ```ruby 112 | # gist 123 113 | def do_thing_from_gist 114 | puts "Doing the thing from the gist" 115 | end 116 | ``` 117 | 118 | ```ruby 119 | Gisture.run('123') do 120 | do_thing_from_gist # this would call the method that was defined in your gist to actually perform the action 121 | end 122 | ``` 123 | 124 | Without the provided block, all that would happen is your method would be defined and available for use. Since the method is never actually called anywhere in the gist, we need to trigger it manually in the block if we want it to be executed. 125 | 126 | ### Execution Strategies 127 | 128 | There are three execution strategies available for running your gists. These are `:eval`, `:load` and `:require`. The strategy you use will depend heavily on what exactly you're trying to do and how the code in your gist is structured. For example, if you're defining modules/classes for use elsewhere in your project, then you probably want to `load` or `require` the gist file, then utilize those classes. If you are performing a lot of inline functional operations that you want to be executed inline or in a background task you'd probably lean towards `eval` or `load`. 129 | 130 | **:eval** 131 | 132 | As the name implies, the eval strategy will evaluate the contents of your gist file (as a string) using ruby's `eval` method. This happens inside a sanitary object called `Gisture::Evaluator`, which is returned and can be manipulated further or used to call any defined methods, etc. 133 | 134 | The eval strategy can be a handy way to take advantage of metaprogramming to create one-off objects with custom functionality. For example, if you were to define the following method in a gist, and then use the eval strategy, the `Gisture::Evaluator` object that was returned would have the method defined as an instance method. 135 | 136 | ```ruby 137 | # gist 123 138 | def do_thing_from_gist 139 | puts "Doing the thing from the gist" 140 | end 141 | ``` 142 | 143 | ```ruby 144 | e = Gisture.run('123', strategy: :eval) # returns a Gisture::Evaluator 145 | e.do_thing_from_gist # prints "Doing the thing from the gist" 146 | ``` 147 | 148 | **:require** 149 | 150 | The require strategy puts the contents of the gist into a tempfile and uses ruby's built-in `require` to include it. This means that all the caveats around how the code is included at runtime apply. If we were to take the same gist from the eval example above, we'd end up with the method defined at the toplevel binding. 151 | 152 | ```ruby 153 | # gist 123 154 | def do_thing_from_gist 155 | puts "Doing the thing from the gist" 156 | end 157 | ``` 158 | 159 | ```ruby 160 | Gisture.run('123', strategy: :require) # returns true (the result of the call to `require`) 161 | do_thing_from_gist # prints "Doing the thing from the gist" 162 | ``` 163 | 164 | **:load** 165 | 166 | The load strategy puts the contents of the gist into a tempfile and uses ruby's built-in `load` to include it. This means that like require, all the caveats around how the code is included at runtime apply. If we were to take the same gist from the eval example above, it would behave the same way as our require example. 167 | 168 | ### Multiple Files 169 | 170 | If your gist contains more than one file, you'll need to tell gisture which file you'd like to run. You can do this by passing the appropriate argument when creating a gist. 171 | 172 | ```ruby 173 | Gisture.new('123', filename: 'myfile.rb') 174 | Gisture::Gist.new('123', filename: 'myfile.rb') 175 | ``` 176 | 177 | ### Rake Task 178 | 179 | Gisture also provides a built-in rake task, named `gisture:run`, to easily run one-off tasks in the background. If you're using rails, the gisture railtie will automatically include the task for you, otherwise you can include the tasks found in `lib/tasks/gisture.rake` however is appropriate for your project. 180 | 181 | You can call the rake task with your gist ID as well as an optional strategy, filename, version and callback string (which will be eval'd as a callback block). 182 | 183 | ``` 184 | rake gisture:run[abc123] 185 | rake gisture:run[abc123,load] 186 | rake gisture:run[abc123,eval,my_file.rb] 187 | rake gisture:run[abc123,eval,my_file.rb,123abc] 188 | rake gisture:run[abc123,load,my_method.rb,123abc,'my_method(whatever)'] 189 | ``` 190 | 191 | To run a YAML gisture from within a repository: 192 | 193 | ``` 194 | rake gisture:run[markrebec/gisture,path/to/gisture.yml] 195 | ``` 196 | 197 | Or for a file in a repository: 198 | 199 | ``` 200 | rake gisture:run[markrebec/gisture,path/to/file.rb] 201 | rake gisture:run[markrebec/gisture,path/to/file.rb,eval] 202 | rake gisture:run[markrebec/gisture,path/to/file.rb,eval,'my_method(whatever)'] 203 | ``` 204 | 205 | ## TODO 206 | 207 | * Add `:exec` strategy to execute the gist file in a separate ruby process 208 | * Allow sources other than gists, like repos containing one-off scripts, generic github blobs, local files, etc. 209 | 210 | ## Contributing 211 | 1. Fork it 212 | 2. Create your feature branch (`git checkout -b my-new-feature`) 213 | 3. Commit your changes (`git commit -am 'Add some feature'`) 214 | 4. Push to the branch (`git push origin my-new-feature`) 215 | 5. Create new Pull Request 216 | --------------------------------------------------------------------------------