gistribute 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cli.rb ADDED
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cli/auth"
4
+ require "cli/install"
5
+ require "cli/upload"
6
+
7
+ module Gistribute
8
+ class CLI
9
+ def initialize
10
+ @options = OptimistXL.options do
11
+ version "gistribute #{File.read(File.expand_path('../VERSION', __dir__)).strip}"
12
+
13
+ banner <<~BANNER
14
+ #{version}
15
+
16
+ Usage:
17
+ gistribute SUBCOMMAND [OPTION]... INPUT
18
+
19
+ Try `gistribute SUBCOMMAND -h` for more info. Available subcommands:
20
+ * login: log into your GitHub account
21
+ * logout: log out of your GitHub account
22
+ * install: download and install files from a gistribution
23
+ * upload: upload a new gistribution
24
+
25
+ Options:
26
+ BANNER
27
+
28
+ opt :version, "display version number"
29
+ opt :help, "display a help message"
30
+
31
+ # Sub-commands can't access the version from this scope for whatever reason
32
+ v = version
33
+
34
+ subcmd :login, "log into your GitHub account"
35
+ subcmd :logout, "log out of your GitHub account"
36
+
37
+ subcmd :install, "install from a gistribution" do
38
+ banner <<~BANNER
39
+ #{v}
40
+
41
+ Usage:
42
+ gistribute install [OPTION]... URL_OR_ID
43
+
44
+ Options:
45
+ BANNER
46
+
47
+ opt :yes, "install files without prompting"
48
+ opt :force, "overwrite existing files without prompting"
49
+ end
50
+
51
+ subcmd :upload, "upload a gistribution" do
52
+ banner <<~BANNER
53
+ #{v}
54
+
55
+ Usage:
56
+ gistribute upload [OPTION]... FILE...
57
+ gistribute upload [OPTION]... DIRECTORY
58
+
59
+ Options:
60
+ BANNER
61
+
62
+ opt :description, "description for the Gist", type: :string
63
+ opt :private, "use a private Gist"
64
+ opt :yes, "upload files without prompting"
65
+ end
66
+
67
+ educate_on_error
68
+ end
69
+
70
+ @subcommand, @global_options, @subcommand_options =
71
+ @options.subcommand, @options.global_options, @options.subcommand_options
72
+
73
+ authenticate unless @subcommand == "logout"
74
+
75
+ case @subcommand
76
+ when "install"
77
+ @gist_input = ARGV.first
78
+ when "upload"
79
+ @files = ARGV.dup
80
+ end
81
+ end
82
+
83
+ def run
84
+ case @subcommand
85
+ when "login"
86
+ # Do nothing, #authenticate is run from the constructor
87
+ when "logout"
88
+ FileUtils.rm_rf CONFIG_FILE
89
+ puts "Logged out.".green
90
+ else
91
+ if ARGV.empty?
92
+ OptimistXL.educate
93
+ end
94
+
95
+ eval @subcommand
96
+ end
97
+ end
98
+
99
+ def confirm?(prompt)
100
+ print prompt
101
+ input = $stdin.gets.strip.downcase
102
+
103
+ input.start_with?("y") || input.empty?
104
+ end
105
+
106
+ def get_input(prompt)
107
+ print prompt
108
+ $stdin.gets.strip
109
+ end
110
+
111
+ # Prints an error message and exits the program.
112
+ def panic!(message)
113
+ $stderr.puts "#{'Error'.red}: #{message}"
114
+ exit 1
115
+ end
116
+ end
117
+ end
data/lib/gistribute.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "colorize"
4
+ require "fileutils"
5
+ require "json"
6
+ require "launchy"
7
+ require "octokit"
8
+ require "optimist_xl"
9
+
10
+ require "cli"
11
+
12
+ CLIENT_ID = "3f37dc8255e5ab891c3d"
13
+ CONFIG_FILE = "#{Dir.home}/.config/gistribute".freeze
14
+
15
+ module Gistribute
16
+ class << self
17
+ # The user may enter either the full URL or just the ID, this function
18
+ # will parse it out of the input.
19
+ def parse_id(str)
20
+ str[%r{(^|/)([[:xdigit:]]+)}, 2]
21
+ end
22
+
23
+ # Encodes a file path for use in the Gist filename
24
+ def encode(filename)
25
+ filename.sub(/^#{Dir.home}/, "~").gsub("/", "|")
26
+ end
27
+
28
+ # Decodes the filename from the Gist into a usable path
29
+ def decode(filename)
30
+ filename.gsub(/[~|]/, "|" => "/", "~" => Dir.home)
31
+ end
32
+ end
33
+ end
data/spec/.rubocop.yml ADDED
@@ -0,0 +1,46 @@
1
+ inherit_from: ../.rubocop.yml
2
+
3
+ RSpec/Focus:
4
+ Severity: warning
5
+
6
+ Style/SignalException:
7
+ Enabled: false
8
+
9
+ # Lots of spacing for alignment in the tests
10
+ Layout/SpaceInsideArrayLiteralBrackets:
11
+ Enabled: false
12
+
13
+ Lint/AmbiguousBlockAssociation:
14
+ Enabled: false
15
+
16
+ ################################################################################
17
+
18
+ RSpec/ContextWording:
19
+ Enabled: false
20
+
21
+ RSpec/EmptyExampleGroup:
22
+ Enabled: false
23
+
24
+ RSpec/ExampleLength:
25
+ Enabled: false
26
+
27
+ RSpec/InstanceVariable:
28
+ Enabled: false
29
+
30
+ RSpec/MultipleExpectations:
31
+ Enabled: false
32
+
33
+ RSpec/NestedGroups:
34
+ Enabled: false
35
+
36
+ RSpec/NoExpectationExample:
37
+ Enabled: false
38
+
39
+ RSpec/ScatteredLet:
40
+ Enabled: false
41
+
42
+ RSpec/SortMetadata:
43
+ Enabled: false
44
+
45
+ RSpec/VerifiedDoubles:
46
+ Enabled: false
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ DEVICE_RES = {
6
+ "device_code" => "testdevicecode",
7
+ "expires_in" => "899",
8
+ "interval" => "1",
9
+ "user_code" => "1337-6969",
10
+ "verification_uri" => "https://github.com/login/device"
11
+ }.freeze
12
+
13
+ PENDING_RES = {
14
+ "error" => "authorization_pending",
15
+ "error_description" => "The authorization request is still pending.",
16
+ "error_uri" => "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow"
17
+ }.freeze
18
+
19
+ EXPIRED_RES = {
20
+ "error" => "expired_token",
21
+ "error_description" => "The `device code` has expired.",
22
+ "error_uri" => "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow"
23
+ }.freeze
24
+
25
+ SUCCESS_RES = {
26
+ "access_token" => OAUTH_TOKEN,
27
+ "scope" => "gist",
28
+ "token_type" => "bearer"
29
+ }.freeze
30
+
31
+ def mock_oauth_response(res)
32
+ allow(Net::HTTP).to receive(:post_form)
33
+ .with(URI("https://github.com/login/oauth/access_token"), anything)
34
+ .and_return(:res2)
35
+ allow(URI).to receive(:decode_www_form)
36
+ .with(:res2)
37
+ .and_return(res)
38
+ end
39
+
40
+ # Hack to get the `:resX` symbols to fall through into #decode_www_form
41
+ class Symbol
42
+ def body
43
+ itself
44
+ end
45
+ end
46
+
47
+ describe Gistribute::CLI do
48
+ before do
49
+ allow(Launchy).to receive(:open)
50
+ allow(File).to receive(:write)
51
+ suppress_stdout
52
+ end
53
+
54
+ let(:http_ok) { Net::HTTPOK.new("1.1", "200", "OK") }
55
+
56
+ describe "#authenticate" do
57
+ context "when there is no access key saved" do
58
+ before do
59
+ allow(File).to receive(:exist?).and_return(false)
60
+
61
+ allow(Net::HTTP).to receive(:post_form)
62
+ .with(URI("https://github.com/login/device/code"), anything)
63
+ .and_return(:res1)
64
+ allow(URI).to receive(:decode_www_form)
65
+ .with(:res1)
66
+ .and_return(DEVICE_RES.to_a)
67
+ end
68
+
69
+ describe "the initial output" do
70
+ before do
71
+ mock_oauth_response SUCCESS_RES
72
+ run "login"
73
+ end
74
+
75
+ it "prints the user code" do
76
+ expect($stdout).to have_received(:write)
77
+ .with(a_string_matching DEVICE_RES["user_code"])
78
+ end
79
+
80
+ it "prints the verification URI" do
81
+ expect($stdout).to have_received(:write)
82
+ .with(a_string_matching DEVICE_RES["verification_uri"])
83
+ end
84
+
85
+ it "opens the verification URI in a browser" do
86
+ expect(Launchy).to have_received(:open).with(DEVICE_RES["verification_uri"])
87
+ end
88
+ end
89
+
90
+ describe "when the response is a success" do
91
+ before do
92
+ mock_oauth_response SUCCESS_RES
93
+ allow(Octokit::Client).to receive(:new).and_call_original
94
+ run "login"
95
+ end
96
+
97
+ it "writes the token to the config file" do
98
+ expect(File).to have_received(:write).with(CONFIG_FILE, OAUTH_TOKEN)
99
+ end
100
+
101
+ it "logs in with Octokit" do
102
+ expect(Octokit::Client).to have_received(:new)
103
+ .with(a_hash_including access_token: OAUTH_TOKEN)
104
+ end
105
+ end
106
+
107
+ context "when the response is timed out" do
108
+ before do
109
+ mock_oauth_response EXPIRED_RES
110
+ %i[puts print].each { |p| allow($stderr).to receive p }
111
+ run "login", fail_on_exit: false
112
+ end
113
+
114
+ let(:error) { "#{'Error'.red}: Token expired! Please try again." }
115
+
116
+ it "displays an error" do
117
+ expect($stderr).to have_received(:puts).with(error)
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ describe "the `logout` subcommand" do
124
+ before do
125
+ allow(FileUtils).to receive(:rm_rf)
126
+ run "logout"
127
+ end
128
+
129
+ it "deletes the auth token" do
130
+ expect(FileUtils).to have_received(:rm_rf).with(CONFIG_FILE)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ NEW_DIR_FILENAME = "nested/test.file"
6
+ MULTI_FILENAMES = ["file1", "dir/file2"].freeze
7
+
8
+ SINGLE_FILE_CONTENTS = "Line 1\nLine 2\n"
9
+
10
+ # Test Gists
11
+ PUB_SINGLE_FILE_ID = "4346763"
12
+ SEC_SINGLE_FILE_ID = "5fa3c6bab88036e95d62cadf15128ec3"
13
+ NO_TITLE_ID = "5865a130f9cf40acd9f0e85d15e601a7"
14
+ NO_PIPE_SPACING_ID = "5677679b0db25054521753e5d59bed3d"
15
+ NON_EXISTENT_DIR_ID = "8c86bf9cda921ebe7ad1bf0c46afb108"
16
+ MULTI_FILE_ID = "8d4a2a4c8fe0b1427fed39c939857a40"
17
+ CWD_ID = "3c7006e629fcc54262ef02a5b2204735"
18
+ HOME_ID = "acb6caa80886101a68c5c85e4c100ddb"
19
+
20
+ def test_single_file(id, path)
21
+ before { run "install", id }
22
+
23
+ # TEMP already gets `rm -rf`ed in the `cli_spec.rb` #after
24
+ unless path == TEMP
25
+ after { FileUtils.rm "#{path}/#{FILENAME}" }
26
+ end
27
+
28
+ let(:file_contents) { File.read "#{path}/#{FILENAME}" }
29
+
30
+ it "downloads the file into #{path}" do
31
+ expect(file_contents).to eq SINGLE_FILE_CONTENTS
32
+ end
33
+ end
34
+
35
+ describe Gistribute::CLI do
36
+ before do
37
+ FileUtils.rm_rf TEMP
38
+ FileUtils.mkdir_p TEMP
39
+ suppress_stdout
40
+ end
41
+
42
+ after { FileUtils.rm_rf TEMP }
43
+
44
+ describe "#install" do
45
+ context "when user inputs `y` at the installation prompt" do
46
+ before { simulate_user_input "y\n" }
47
+
48
+ {
49
+ "public single file": PUB_SINGLE_FILE_ID,
50
+ "secret single file": SEC_SINGLE_FILE_ID,
51
+ "no title": NO_TITLE_ID,
52
+ "no || spacing": NO_PIPE_SPACING_ID
53
+ }.each do |description, id|
54
+ context "when run with a #{description} Gist" do
55
+ test_single_file id, TEMP
56
+ end
57
+ end
58
+
59
+ context "when given a directory that doesn't exist" do
60
+ before { run "install", NON_EXISTENT_DIR_ID }
61
+
62
+ let(:file_contents) { File.read "#{TEMP}/#{NEW_DIR_FILENAME}" }
63
+
64
+ it "creates the directory" do
65
+ expect(file_contents).to eq SINGLE_FILE_CONTENTS
66
+ end
67
+ end
68
+
69
+ context "when run with a multi-file Gist" do
70
+ before { run "install", MULTI_FILE_ID }
71
+
72
+ let(:file1_contents) { File.read "#{TEMP}/#{MULTI_FILENAMES[0]}" }
73
+ let(:file2_contents) { File.read "#{TEMP}/#{MULTI_FILENAMES[1]}" }
74
+
75
+ it "downloads the files into the correct locations" do
76
+ [file1_contents, file2_contents].each_with_index do |result, i|
77
+ file_number = i + 1
78
+ expect(result).to eq "F#{file_number}L1\nF#{file_number}L2\n"
79
+ end
80
+ end
81
+ end
82
+
83
+ context "when given a file for the current working directory" do
84
+ test_single_file CWD_ID, Dir.pwd
85
+ end
86
+
87
+ context "when given a file for the home directory" do
88
+ test_single_file HOME_ID, Dir.home
89
+ end
90
+
91
+ context "when given a bad ID (404)" do
92
+ before do
93
+ %i[puts print].each { |p| allow($stderr).to receive p }
94
+ run "install", "bad", fail_on_exit: false
95
+ end
96
+
97
+ it "prints the error to STDERR" do
98
+ expect($stderr).to have_received(:print).with <<~EOS.chop.red
99
+ \rThere was an error downloading the requested Gist.
100
+ The error is as follows:
101
+ EOS
102
+
103
+ expect($stderr).to have_received(:puts).with(" 404 Not Found")
104
+ expect($stderr).to have_received(:print).with("The ID that was queried is: ".red)
105
+ expect($stderr).to have_received(:puts).with("bad")
106
+ end
107
+ end
108
+ end
109
+
110
+ context "when user inputs nothing at the installation prompt" do
111
+ before do
112
+ simulate_user_input "\n"
113
+ run "install", PUB_SINGLE_FILE_ID
114
+ end
115
+
116
+ it "saves the file" do
117
+ expect(File).to exist("#{TEMP}/#{FILENAME}")
118
+ end
119
+ end
120
+
121
+ %w[n m].each do |ch|
122
+ context "when user inputs `#{ch}` at the installation prompt" do
123
+ before do
124
+ simulate_user_input "#{ch}\n"
125
+ run "install", PUB_SINGLE_FILE_ID
126
+ end
127
+
128
+ it "doesn't save the file" do
129
+ expect(File).not_to exist("#{TEMP}/#{FILENAME}")
130
+ end
131
+ end
132
+ end
133
+
134
+ context "when a file would be overwritten" do
135
+ let(:orig_content) { "original" }
136
+
137
+ before do
138
+ FileUtils.mkdir_p "#{TEMP}/dir"
139
+ MULTI_FILENAMES.each do |filename|
140
+ File.write("#{TEMP}/#{filename}", orig_content)
141
+ end
142
+ end
143
+
144
+ let(:file1_contents) { File.read "#{TEMP}/#{MULTI_FILENAMES[0]}" }
145
+ let(:file2_contents) { File.read "#{TEMP}/#{MULTI_FILENAMES[1]}" }
146
+
147
+ context "when the user inputs `y` at the file overwrite prompts" do
148
+ before do
149
+ simulate_user_input "y\n", "y\n", "y\n"
150
+ run "install", MULTI_FILE_ID
151
+ end
152
+
153
+ it "downloads the files into the correct locations" do
154
+ expect([file1_contents, file2_contents]).not_to include orig_content
155
+ end
156
+ end
157
+
158
+ context "when the user inputs `n` at the file overwrite prompts" do
159
+ before do
160
+ simulate_user_input "y\n", "n\n", "n\n"
161
+ run "install", MULTI_FILE_ID
162
+ end
163
+
164
+ it "doesn't download the files" do
165
+ expect([file1_contents, file2_contents]).to all eq orig_content
166
+ end
167
+ end
168
+
169
+ context "with the `--force` flag" do
170
+ before do
171
+ simulate_user_input "y\n"
172
+ run "install", "--force", MULTI_FILE_ID
173
+ end
174
+
175
+ it "overwrites the files without prompting" do
176
+ [file1_contents, file2_contents].each do |result|
177
+ expect(result).not_to eq orig_content
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ context "when ran with the --yes flag" do
184
+ before { run "install", "--yes", PUB_SINGLE_FILE_ID }
185
+
186
+ let(:file_contents) { File.read "#{TEMP}/#{FILENAME}" }
187
+
188
+ it "downloads the file without prompting" do
189
+ expect(file_contents).to eq SINGLE_FILE_CONTENTS
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Gistribute::CLI do
6
+ before { suppress_stdout }
7
+
8
+ %w[install upload].each do |subcmd|
9
+ describe "the `#{subcmd}` subcommand" do
10
+ context "when no argument is provided" do
11
+ it "shows the help screen" do
12
+ allow(OptimistXL).to receive(:educate).and_call_original
13
+ run subcmd, fail_on_exit: false
14
+
15
+ expect(OptimistXL).to have_received :educate
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ context "when run with the `--version` flag" do
22
+ let :version do
23
+ File.read(File.expand_path("../../VERSION", __dir__)).strip
24
+ end
25
+
26
+ it "outputs the version number" do
27
+ expect { run "--version", fail_on_exit: false }
28
+ .to output(a_string_matching(version)).to_stdout
29
+ end
30
+ end
31
+ end