gistl 4.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/Gemfile +3 -0
- data/LICENSE.MIT +7 -0
- data/README.md +189 -0
- data/Rakefile +44 -0
- data/bin/gist +194 -0
- data/build/gist +1998 -0
- data/build/gist.1 +246 -0
- data/gistl.gemspec +21 -0
- data/lib/gist.rb +566 -0
- data/spec/auth_token_file_spec.rb +61 -0
- data/spec/clipboard_spec.rb +40 -0
- data/spec/ghe_spec.rb +91 -0
- data/spec/gist_spec.rb +23 -0
- data/spec/proxy_spec.rb +21 -0
- data/spec/rawify_spec.rb +12 -0
- data/spec/shorten_spec.rb +11 -0
- data/spec/spec_helper.rb +19 -0
- data/vendor/json.rb +1304 -0
- metadata +121 -0
data/build/gist.1
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
.\" generated with Ronn/v0.7.3
|
2
|
+
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
3
|
+
.
|
4
|
+
.TH "GIST" "1" "July 2015" "" "Gist manual"
|
5
|
+
.
|
6
|
+
.SH "NAME"
|
7
|
+
\fBgist\fR \- upload code to https://gist\.github\.com
|
8
|
+
.
|
9
|
+
.SH "Synopsis"
|
10
|
+
The gist gem provides a \fBgist\fR command that you can use from your terminal to upload content to https://gist\.github\.com/\.
|
11
|
+
.
|
12
|
+
.SH "Installation"
|
13
|
+
.
|
14
|
+
.IP "\(bu" 4
|
15
|
+
If you have ruby installed:
|
16
|
+
.
|
17
|
+
.IP
|
18
|
+
gem install gist
|
19
|
+
.
|
20
|
+
.IP "\(bu" 4
|
21
|
+
If you\'re using Bundler:
|
22
|
+
.
|
23
|
+
.IP
|
24
|
+
source :rubygems gem \'gist\'
|
25
|
+
.
|
26
|
+
.IP "\(bu" 4
|
27
|
+
For OS X, gist lives in Homebrew
|
28
|
+
.
|
29
|
+
.IP
|
30
|
+
brew install gist
|
31
|
+
.
|
32
|
+
.IP "" 0
|
33
|
+
.
|
34
|
+
.SH "Command"
|
35
|
+
.
|
36
|
+
.IP "\(bu" 4
|
37
|
+
To upload the contents of \fBa\.rb\fR just:
|
38
|
+
.
|
39
|
+
.IP
|
40
|
+
gist a\.rb
|
41
|
+
.
|
42
|
+
.IP "\(bu" 4
|
43
|
+
Upload multiple files:
|
44
|
+
.
|
45
|
+
.IP
|
46
|
+
gist a b c gist *\.rb
|
47
|
+
.
|
48
|
+
.IP "\(bu" 4
|
49
|
+
By default it reads from STDIN, and you can set a filename with \fB\-f\fR\.
|
50
|
+
.
|
51
|
+
.IP
|
52
|
+
gist \-f test\.rb <a\.rb
|
53
|
+
.
|
54
|
+
.IP "\(bu" 4
|
55
|
+
Alternatively, you can just paste from the clipboard:
|
56
|
+
.
|
57
|
+
.IP
|
58
|
+
gist \-P
|
59
|
+
.
|
60
|
+
.IP "\(bu" 4
|
61
|
+
Use \fB\-p\fR to make the gist private:
|
62
|
+
.
|
63
|
+
.IP
|
64
|
+
gist \-p a\.rb
|
65
|
+
.
|
66
|
+
.IP "\(bu" 4
|
67
|
+
Use \fB\-d\fR to add a description:
|
68
|
+
.
|
69
|
+
.IP
|
70
|
+
gist \-d "Random rbx bug" a\.rb
|
71
|
+
.
|
72
|
+
.IP "\(bu" 4
|
73
|
+
You can update existing gists with \fB\-u\fR:
|
74
|
+
.
|
75
|
+
.IP
|
76
|
+
gist \-u GIST_ID FILE_NAME gist \-u 42f2c239d2eb57299408 test\.txt
|
77
|
+
.
|
78
|
+
.IP "\(bu" 4
|
79
|
+
If you\'d like to copy the resulting URL to your clipboard, use \fB\-c\fR\.
|
80
|
+
.
|
81
|
+
.IP
|
82
|
+
gist \-c <a\.rb
|
83
|
+
.
|
84
|
+
.IP "\(bu" 4
|
85
|
+
If you\'d like to copy the resulting embeddable URL to your clipboard, use \fB\-e\fR\.
|
86
|
+
.
|
87
|
+
.IP
|
88
|
+
gist \-e <a\.rb
|
89
|
+
.
|
90
|
+
.IP "\(bu" 4
|
91
|
+
And you can just ask gist to open a browser window directly with \fB\-o\fR\.
|
92
|
+
.
|
93
|
+
.IP
|
94
|
+
gist \-o <a\.rb
|
95
|
+
.
|
96
|
+
.IP "\(bu" 4
|
97
|
+
To list (public gists or all gists for authed user) gists for user
|
98
|
+
.
|
99
|
+
.IP
|
100
|
+
gist \-l : all gists for authed user gist \-l defunkt : list defunkt\'s public gists
|
101
|
+
.
|
102
|
+
.IP "\(bu" 4
|
103
|
+
See \fBgist \-\-help\fR for more detail\.
|
104
|
+
.
|
105
|
+
.IP "" 0
|
106
|
+
.
|
107
|
+
.SH "Login"
|
108
|
+
If you want to associate your gists with your GitHub account, you need to login with gist\. It doesn\'t store your username and password, it just uses them to get an OAuth2 token (with the "gist" permission)\.
|
109
|
+
.
|
110
|
+
.IP "" 4
|
111
|
+
.
|
112
|
+
.nf
|
113
|
+
|
114
|
+
gist \-\-login
|
115
|
+
Obtaining OAuth2 access_token from github\.
|
116
|
+
GitHub username: ConradIrwin
|
117
|
+
GitHub password:
|
118
|
+
2\-factor auth code:
|
119
|
+
Success! https://github\.com/settings/applications
|
120
|
+
.
|
121
|
+
.fi
|
122
|
+
.
|
123
|
+
.IP "" 0
|
124
|
+
.
|
125
|
+
.P
|
126
|
+
You can read the 2\-factor auth code from an sms or the authentication app, depending on how you set your account up \fIhttps://github\.com/settings/admin\fR\.
|
127
|
+
.
|
128
|
+
.P
|
129
|
+
Note: 2\-factor authentication just appeared recently \fIhttps://github\.com/blog/1614\-two\-factor\-authentication\fR, so if you run into errors, update the gist gem\.
|
130
|
+
.
|
131
|
+
.IP "" 4
|
132
|
+
.
|
133
|
+
.nf
|
134
|
+
|
135
|
+
gem update gist
|
136
|
+
.
|
137
|
+
.fi
|
138
|
+
.
|
139
|
+
.IP "" 0
|
140
|
+
.
|
141
|
+
.P
|
142
|
+
This token is stored in \fB~/\.gist\fR and used for all future gisting\. If you need to you can revoke it from https://github\.com/settings/applications, or just delete the file\. If you need to store tokens for both github\.com and a Github Enterprise instance you can save your Github Enterprise token in \fB~/\.gist\.github\.example\.com\fR where "github\.example\.com" is the URL for your Github Enterprise instance\.
|
143
|
+
.
|
144
|
+
.IP "\(bu" 4
|
145
|
+
After you\'ve done this, you can still upload gists anonymously with \fB\-a\fR\.
|
146
|
+
.
|
147
|
+
.IP
|
148
|
+
gist \-a a\.rb
|
149
|
+
.
|
150
|
+
.IP "" 0
|
151
|
+
.
|
152
|
+
.TP
|
153
|
+
You can also use Gist as a library from inside your ruby code:
|
154
|
+
.
|
155
|
+
.IP
|
156
|
+
Gist\.gist("Look\.at(:my => \'awesome\')\.code")
|
157
|
+
.
|
158
|
+
.P
|
159
|
+
If you need more advanced features you can also pass:
|
160
|
+
.
|
161
|
+
.IP "\(bu" 4
|
162
|
+
\fB:access_token\fR to authenticate using OAuth2 (default is `File\.read("~/\.gist"))\.
|
163
|
+
.
|
164
|
+
.IP "\(bu" 4
|
165
|
+
\fB:filename\fR to change the syntax highlighting (default is \fBa\.rb\fR)\.
|
166
|
+
.
|
167
|
+
.IP "\(bu" 4
|
168
|
+
\fB:public\fR if you want your gist to have a guessable url\.
|
169
|
+
.
|
170
|
+
.IP "\(bu" 4
|
171
|
+
\fB:description\fR to add a description to your gist\.
|
172
|
+
.
|
173
|
+
.IP "\(bu" 4
|
174
|
+
\fB:update\fR to update an existing gist (can be a URL or an id)\.
|
175
|
+
.
|
176
|
+
.IP "\(bu" 4
|
177
|
+
\fB:anonymous\fR to submit an anonymous gist (default is false)\.
|
178
|
+
.
|
179
|
+
.IP "\(bu" 4
|
180
|
+
\fB:copy\fR to copy the resulting URL to the clipboard (default is false)\.
|
181
|
+
.
|
182
|
+
.IP "\(bu" 4
|
183
|
+
\fB:open\fR to open the resulting URL in a browser (default is false)\.
|
184
|
+
.
|
185
|
+
.IP "" 0
|
186
|
+
.
|
187
|
+
.P
|
188
|
+
NOTE: The access_token must have the "gist" scope\.
|
189
|
+
.
|
190
|
+
.IP "\(bu" 4
|
191
|
+
If you want to upload multiple files in the same gist, you can:
|
192
|
+
.
|
193
|
+
.IP
|
194
|
+
Gist\.multi_gist("a\.rb" => "Foo\.bar", "a\.py" => "Foo\.bar")
|
195
|
+
.
|
196
|
+
.IP "\(bu" 4
|
197
|
+
If you\'d rather use gist\'s builtin access_token, then you can force the user to obtain one by calling:
|
198
|
+
.
|
199
|
+
.IP
|
200
|
+
Gist\.login!
|
201
|
+
.
|
202
|
+
.IP "\(bu" 4
|
203
|
+
This will take them through the process of obtaining an OAuth2 token, and storing it in \fB~/\.gist\fR, where it can later be read by \fBGist\.gist\fR
|
204
|
+
.
|
205
|
+
.IP "" 0
|
206
|
+
.
|
207
|
+
.SH "GitHub enterprise"
|
208
|
+
.
|
209
|
+
.IP "\(bu" 4
|
210
|
+
If you\'d like \fBgist\fR to use your locally installed GitHub Enterprise \fIhttps://enterprise\.github\.com/\fR, you need to export the \fBGITHUB_URL\fR environment variable in your \fB~/\.bashrc\fR\.
|
211
|
+
.
|
212
|
+
.IP
|
213
|
+
export GITHUB_URL=http://github\.internal\.example\.com/
|
214
|
+
.
|
215
|
+
.IP "\(bu" 4
|
216
|
+
Once you\'ve done this and restarted your terminal (or run \fBsource ~/\.bashrc\fR), gist will automatically use github enterprise instead of the public github\.com
|
217
|
+
.
|
218
|
+
.IP "" 0
|
219
|
+
.
|
220
|
+
.SH "Configuration"
|
221
|
+
.
|
222
|
+
.IP "\(bu" 4
|
223
|
+
If you\'d like \fB\-o\fR or \fB\-c\fR to be the default when you use the gist executable, add an alias to your \fB~/\.bashrc\fR (or equivalent)\. For example:
|
224
|
+
.
|
225
|
+
.IP
|
226
|
+
alias gist=\'gist \-c\'
|
227
|
+
.
|
228
|
+
.IP "\(bu" 4
|
229
|
+
If you\'d prefer gist to open a different browser, then you can export the BROWSER environment variable:
|
230
|
+
.
|
231
|
+
.IP
|
232
|
+
export BROWSER=google\-chrome
|
233
|
+
.
|
234
|
+
.IP "" 0
|
235
|
+
.
|
236
|
+
.P
|
237
|
+
If clipboard or browser integration don\'t work on your platform, please file a bug or (more ideally) a pull request\.
|
238
|
+
.
|
239
|
+
.P
|
240
|
+
If you need to use an HTTP proxy to access the internet, export the \fBHTTP_PROXY\fR or \fBhttp_proxy\fR environment variable and gist will use it\.
|
241
|
+
.
|
242
|
+
.SH "Meta\-fu"
|
243
|
+
Thanks to @defunkt and @indirect for writing and maintaining versions 1 through 3\. Thanks to @rking and @ConradIrwin for maintaining version 4\.
|
244
|
+
.
|
245
|
+
.P
|
246
|
+
Licensed under the MIT license\. Bug\-reports, and pull requests are welcome\.
|
data/gistl.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require './lib/gist'
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'gistl'
|
5
|
+
s.version = Gist::VERSION
|
6
|
+
s.summary = 'CLI/API gist client'
|
7
|
+
s.description = 'Enhanced fork of defunkt/gist. See `gist --help`.'
|
8
|
+
s.homepage = 'https://github.com/zmwangx/gist'
|
9
|
+
s.email = ['zmwangx@gmail.com']
|
10
|
+
s.authors = ['Zhiming Wang']
|
11
|
+
s.license = 'MIT'
|
12
|
+
s.files = `git ls-files`.split("\n")
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
|
15
|
+
s.executables << 'gist'
|
16
|
+
|
17
|
+
s.add_development_dependency 'rake'
|
18
|
+
s.add_development_dependency 'ronn'
|
19
|
+
s.add_development_dependency 'webmock'
|
20
|
+
s.add_development_dependency 'rspec', '>3'
|
21
|
+
end
|
data/lib/gist.rb
ADDED
@@ -0,0 +1,566 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'cgi'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'json'
|
8
|
+
rescue LoadError
|
9
|
+
require File.join File.dirname(File.dirname(__FILE__)), 'vendor', 'json.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
# It just gists.
|
13
|
+
module Gist
|
14
|
+
extend self
|
15
|
+
|
16
|
+
VERSION = '4.14.2'
|
17
|
+
|
18
|
+
# A list of clipboard commands with copy and paste support.
|
19
|
+
CLIPBOARD_COMMANDS = {
|
20
|
+
'xclip' => 'xclip -o',
|
21
|
+
'xsel -i' => 'xsel -o',
|
22
|
+
'pbcopy' => 'pbpaste',
|
23
|
+
'putclip' => 'getclip'
|
24
|
+
}
|
25
|
+
|
26
|
+
GITHUB_API_URL = URI("https://api.github.com/")
|
27
|
+
GIT_IO_URL = URI("http://git.io")
|
28
|
+
|
29
|
+
GITHUB_BASE_PATH = ""
|
30
|
+
GHE_BASE_PATH = "/api/v3"
|
31
|
+
|
32
|
+
URL_ENV_NAME = "GITHUB_URL"
|
33
|
+
|
34
|
+
USER_AGENT = "gist/#{VERSION} (Net::HTTP, #{RUBY_DESCRIPTION})"
|
35
|
+
|
36
|
+
# Exception tag for errors raised while gisting.
|
37
|
+
module Error;
|
38
|
+
def self.exception(*args)
|
39
|
+
RuntimeError.new(*args).extend(self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
class ClipboardError < RuntimeError; include Error end
|
43
|
+
|
44
|
+
# helper module for authentication token actions
|
45
|
+
module AuthTokenFile
|
46
|
+
def self.filename
|
47
|
+
if ENV.key?("XDG_DATA_HOME") && ! ENV["XDG_DATA_HOME"].empty?
|
48
|
+
data_home = "#{ENV["XDG_DATA_HOME"]}/gist"
|
49
|
+
else
|
50
|
+
data_home = File.expand_path "~/.local/share/gist"
|
51
|
+
end
|
52
|
+
|
53
|
+
FileUtils.mkdir_p data_home, :mode => 0700
|
54
|
+
|
55
|
+
if ENV.key?(URL_ENV_NAME)
|
56
|
+
"#{data_home}/token.#{ENV[URL_ENV_NAME].gsub(/[^a-z.]/, '')}"
|
57
|
+
else
|
58
|
+
"#{data_home}/token"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.read
|
63
|
+
File.read(filename).chomp
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.write(token)
|
67
|
+
File.open(filename, 'w', 0600) do |f|
|
68
|
+
f.write token
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# auth token for authentication
|
74
|
+
#
|
75
|
+
# @return [String] string value of access token or `nil`, if not found
|
76
|
+
def auth_token
|
77
|
+
@token ||= AuthTokenFile.read rescue nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# Upload a gist to https://gist.github.com
|
81
|
+
#
|
82
|
+
# @param [String] content the code you'd like to gist
|
83
|
+
# @param [Hash] options more detailed options, see
|
84
|
+
# the documentation for {multi_gist}
|
85
|
+
#
|
86
|
+
# @see http://developer.github.com/v3/gists/
|
87
|
+
def gist(content, options = {})
|
88
|
+
filename = options[:filename] || "a.rb"
|
89
|
+
multi_gist({filename => content}, options)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Upload a gist to https://gist.github.com
|
93
|
+
#
|
94
|
+
# @param [Hash] files the code you'd like to gist: filename => content
|
95
|
+
# @param [Hash] options more detailed options
|
96
|
+
#
|
97
|
+
# @option options [String] :description the description
|
98
|
+
# @option options [Boolean] :public (false) is this gist public
|
99
|
+
# @option options [Boolean] :anonymous (false) is this gist anonymous
|
100
|
+
# @option options [String] :access_token (`File.read("#{ENV["XDG_DATA_HOME"]}/gist/token")`) The OAuth2 access token.
|
101
|
+
# @option options [String] :update the URL or id of a gist to update
|
102
|
+
# @option options [Boolean] :copy (false) Copy resulting URL to clipboard, if successful.
|
103
|
+
# @option options [Boolean] :open (false) Open the resulting URL in a browser.
|
104
|
+
# @option options [Symbol] :output (:all) The type of return value you'd like:
|
105
|
+
# :html_url gives a String containing the url to the gist in a browser
|
106
|
+
# :short_url gives a String contianing a git.io url that redirects to html_url
|
107
|
+
# :javascript gives a String containing a script tag suitable for embedding the gist
|
108
|
+
# :all gives a Hash containing the parsed json response from the server
|
109
|
+
#
|
110
|
+
# @return [String, Hash] the return value as configured by options[:output]
|
111
|
+
# @raise [Gist::Error] if something went wrong
|
112
|
+
#
|
113
|
+
# @see http://developer.github.com/v3/gists/
|
114
|
+
def multi_gist(files, options={})
|
115
|
+
json = {}
|
116
|
+
|
117
|
+
json[:description] = options[:description] if options[:description]
|
118
|
+
json[:public] = !!options[:public]
|
119
|
+
json[:files] = {}
|
120
|
+
|
121
|
+
files.each_pair do |(name, content)|
|
122
|
+
raise "Cannot gist empty files" if content.to_s.strip == ""
|
123
|
+
json[:files][File.basename(name)] = {:content => content}
|
124
|
+
end
|
125
|
+
|
126
|
+
existing_gist = options[:update].to_s.split("/").last
|
127
|
+
if options[:anonymous]
|
128
|
+
access_token = nil
|
129
|
+
else
|
130
|
+
access_token = (options[:access_token] || auth_token())
|
131
|
+
end
|
132
|
+
|
133
|
+
url = "#{base_path}/gists"
|
134
|
+
url << "/" << CGI.escape(existing_gist) if existing_gist.to_s != ''
|
135
|
+
url << "?access_token=" << CGI.escape(access_token) if access_token.to_s != ''
|
136
|
+
|
137
|
+
request = Net::HTTP::Post.new(url)
|
138
|
+
request.body = JSON.dump(json)
|
139
|
+
request.content_type = 'application/json'
|
140
|
+
|
141
|
+
retried = false
|
142
|
+
|
143
|
+
begin
|
144
|
+
response = http(api_url, request)
|
145
|
+
if Net::HTTPSuccess === response
|
146
|
+
on_success(response.body, options)
|
147
|
+
else
|
148
|
+
raise "Got #{response.class} from gist: #{response.body}"
|
149
|
+
end
|
150
|
+
rescue => e
|
151
|
+
raise if retried
|
152
|
+
retried = true
|
153
|
+
retry
|
154
|
+
end
|
155
|
+
|
156
|
+
rescue => e
|
157
|
+
raise e.extend Error
|
158
|
+
end
|
159
|
+
|
160
|
+
# List gists (private also) for authenticated user
|
161
|
+
# otherwise list public gists for given username (optional argument)
|
162
|
+
#
|
163
|
+
# This method does not handle pagination.
|
164
|
+
#
|
165
|
+
# @param [String] user
|
166
|
+
# @deprecated
|
167
|
+
#
|
168
|
+
# see https://developer.github.com/v3/gists/#list-gists
|
169
|
+
def list_gists(user = "")
|
170
|
+
url = "#{base_path}"
|
171
|
+
|
172
|
+
if user == ""
|
173
|
+
access_token = auth_token()
|
174
|
+
if access_token.to_s != ''
|
175
|
+
url << "/gists?access_token=" << CGI.escape(access_token)
|
176
|
+
|
177
|
+
request = Net::HTTP::Get.new(url)
|
178
|
+
response = http(api_url, request)
|
179
|
+
|
180
|
+
pretty_gist(response)
|
181
|
+
|
182
|
+
else
|
183
|
+
raise Error, "Not authenticated. Use 'gist --login' to login or 'gist -l username' to view public gists."
|
184
|
+
end
|
185
|
+
|
186
|
+
else
|
187
|
+
url << "/users/#{user}/gists"
|
188
|
+
|
189
|
+
request = Net::HTTP::Get.new(url)
|
190
|
+
response = http(api_url, request)
|
191
|
+
|
192
|
+
pretty_gist(response)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# List all gists (private also) for authenticated user
|
197
|
+
# otherwise list public gists for given username (optional argument)
|
198
|
+
#
|
199
|
+
# @param [String] user
|
200
|
+
# @param [Hash] options more detailed options
|
201
|
+
#
|
202
|
+
# @option options [DateTime] :since only list gists updated at or after this time
|
203
|
+
# @option options [Integer] :max_number max number of gists to list
|
204
|
+
# @option options [Integer] :max_pages max number of pages to list
|
205
|
+
#
|
206
|
+
# see https://developer.github.com/v3/gists/#list-gists
|
207
|
+
def list_all_gists(user="", options={})
|
208
|
+
url = "#{base_path}"
|
209
|
+
|
210
|
+
# API base endpoint
|
211
|
+
if user == ""
|
212
|
+
url << "/gists"
|
213
|
+
else
|
214
|
+
url << "/users/#{user}/gists"
|
215
|
+
end
|
216
|
+
|
217
|
+
# pagination
|
218
|
+
url << "?per_page=100"
|
219
|
+
|
220
|
+
# since
|
221
|
+
if options[:since]
|
222
|
+
url << "&since=#{options[:since].iso8601}"
|
223
|
+
end
|
224
|
+
|
225
|
+
# authentication
|
226
|
+
access_token = auth_token()
|
227
|
+
if access_token.to_s != ''
|
228
|
+
url << "&access_token=" << CGI.escape(access_token)
|
229
|
+
elsif user == ""
|
230
|
+
raise Error, "Not authenticated. Use 'gist --login' to login or 'gist -l username' to view public gists."
|
231
|
+
end
|
232
|
+
|
233
|
+
max_number = options[:max_number] ? options[:max_number] : 0
|
234
|
+
max_pages = options[:max_pages] ? options[:max_pages] : 0
|
235
|
+
|
236
|
+
get_gist_pages(url, max_number, max_pages)
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
# List gists given an API endpoint
|
241
|
+
#
|
242
|
+
# Pagination is handled, and all gists are listed by default.
|
243
|
+
#
|
244
|
+
# @param [String] url API endpoint
|
245
|
+
# @param [Integer] max_number max number of gists to list (zero or negative for unlimited)
|
246
|
+
# @param [Integer] max_pages max number of pages to list (zero or negative for unlimited)
|
247
|
+
#
|
248
|
+
# see https://developer.github.com/v3/gists/#list-gists
|
249
|
+
def get_gist_pages(url, max_number=0, max_pages=0)
|
250
|
+
|
251
|
+
request = Net::HTTP::Get.new(url)
|
252
|
+
response = http(api_url, request)
|
253
|
+
number_listed = pretty_gist(response, max_number)
|
254
|
+
|
255
|
+
if ((max_number > 0 && number_listed >= max_number) || max_pages == 1)
|
256
|
+
return
|
257
|
+
end
|
258
|
+
|
259
|
+
link_header = response.header['link']
|
260
|
+
|
261
|
+
if link_header
|
262
|
+
links = Hash[ link_header.gsub(/(<|>|")/, "").split(',').map { |link| link.split('; rel=') } ].invert
|
263
|
+
get_gist_pages(links['next'], max_number - number_listed, max_pages - 1) if links['next']
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
# Print prettified string result of response body for all gists
|
269
|
+
#
|
270
|
+
# @params [Net::HTTPResponse] response
|
271
|
+
# @params [Integer] max_number max number of gists to print (zero or negative for unlimited)
|
272
|
+
# @return [Integer] number of gists printed
|
273
|
+
#
|
274
|
+
# see https://developer.github.com/v3/gists/#response
|
275
|
+
def pretty_gist(response, max_number=0)
|
276
|
+
body = JSON.parse(response.body)
|
277
|
+
if response.code == '200'
|
278
|
+
number_printed = 0
|
279
|
+
(max_number > 0 ? body.take(max_number) : body).each do |gist|
|
280
|
+
description = "#{gist['description'] || gist['files'].keys.join(" ")} #{gist['public'] ? '' : '(secret)'}"
|
281
|
+
puts "#{gist['html_url']} #{description.tr("\n", " ")}\n"
|
282
|
+
$stdout.flush
|
283
|
+
|
284
|
+
number_printed += 1
|
285
|
+
end
|
286
|
+
return number_printed
|
287
|
+
|
288
|
+
else
|
289
|
+
raise Error, body['message']
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Convert long github urls into short git.io ones
|
294
|
+
#
|
295
|
+
# @param [String] url
|
296
|
+
# @return [String] shortened url, or long url if shortening fails
|
297
|
+
def shorten(url)
|
298
|
+
request = Net::HTTP::Post.new("/")
|
299
|
+
request.set_form_data(:url => url)
|
300
|
+
response = http(GIT_IO_URL, request)
|
301
|
+
case response.code
|
302
|
+
when "201"
|
303
|
+
response['Location']
|
304
|
+
else
|
305
|
+
url
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# Convert github url into raw file url
|
310
|
+
#
|
311
|
+
# Unfortunately the url returns from github's api is legacy,
|
312
|
+
# we have to taking a HTTPRedirection before appending it
|
313
|
+
# with '/raw'. Let's looking forward for github's api fix :)
|
314
|
+
#
|
315
|
+
# @param [String] url
|
316
|
+
# @return [String] the raw file url
|
317
|
+
def rawify(url)
|
318
|
+
uri = URI(url)
|
319
|
+
request = Net::HTTP::Get.new(uri.path)
|
320
|
+
response = http(uri, request)
|
321
|
+
if Net::HTTPSuccess === response
|
322
|
+
url + '/raw'
|
323
|
+
elsif Net::HTTPRedirection === response
|
324
|
+
rawify(response.header['location'])
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Log the user into gist.
|
329
|
+
#
|
330
|
+
# This method asks the user for a username and password, and tries to obtain
|
331
|
+
# and OAuth2 access token, which is then stored in $XDG_DATA_HOME/gist/token
|
332
|
+
#
|
333
|
+
# @raise [Gist::Error] if something went wrong
|
334
|
+
# @param [Hash] credentials login details
|
335
|
+
# @option credentials [String] :username
|
336
|
+
# @option credentials [String] :password
|
337
|
+
# @see http://developer.github.com/v3/oauth/
|
338
|
+
def login!(credentials={})
|
339
|
+
puts "Obtaining OAuth2 access_token from github."
|
340
|
+
loop do
|
341
|
+
print "GitHub username: "
|
342
|
+
username = credentials[:username] || $stdin.gets.strip
|
343
|
+
print "GitHub password: "
|
344
|
+
password = credentials[:password] || begin
|
345
|
+
`stty -echo` rescue nil
|
346
|
+
$stdin.gets.strip
|
347
|
+
ensure
|
348
|
+
`stty echo` rescue nil
|
349
|
+
end
|
350
|
+
puts ""
|
351
|
+
|
352
|
+
request = Net::HTTP::Post.new("#{base_path}/authorizations")
|
353
|
+
request.body = JSON.dump({
|
354
|
+
:scopes => [:gist],
|
355
|
+
:note => "The gist gem (#{Time.now})",
|
356
|
+
:note_url => "https://github.com/ConradIrwin/gist"
|
357
|
+
})
|
358
|
+
request.content_type = 'application/json'
|
359
|
+
request.basic_auth(username, password)
|
360
|
+
|
361
|
+
response = http(api_url, request)
|
362
|
+
|
363
|
+
if Net::HTTPUnauthorized === response && response['X-GitHub-OTP']
|
364
|
+
print "2-factor auth code: "
|
365
|
+
twofa_code = $stdin.gets.strip
|
366
|
+
puts ""
|
367
|
+
|
368
|
+
request['X-GitHub-OTP'] = twofa_code
|
369
|
+
response = http(api_url, request)
|
370
|
+
end
|
371
|
+
|
372
|
+
if Net::HTTPCreated === response
|
373
|
+
AuthTokenFile.write JSON.parse(response.body)['token']
|
374
|
+
puts "Success! #{ENV[URL_ENV_NAME] || "https://github.com/"}settings/applications"
|
375
|
+
return
|
376
|
+
elsif Net::HTTPUnauthorized === response
|
377
|
+
puts "Error: #{JSON.parse(response.body)['message']}"
|
378
|
+
next
|
379
|
+
else
|
380
|
+
raise "Got #{response.class} from gist: #{response.body}"
|
381
|
+
end
|
382
|
+
end
|
383
|
+
rescue => e
|
384
|
+
raise e.extend Error
|
385
|
+
end
|
386
|
+
|
387
|
+
# Return HTTP connection
|
388
|
+
#
|
389
|
+
# @param [URI::HTTP] The URI to which to connect
|
390
|
+
# @return [Net::HTTP]
|
391
|
+
def http_connection(uri)
|
392
|
+
env = ENV['http_proxy'] || ENV['HTTP_PROXY']
|
393
|
+
connection = if env
|
394
|
+
proxy = URI(env)
|
395
|
+
Net::HTTP::Proxy(proxy.host, proxy.port).new(uri.host, uri.port)
|
396
|
+
else
|
397
|
+
Net::HTTP.new(uri.host, uri.port)
|
398
|
+
end
|
399
|
+
if uri.scheme == "https"
|
400
|
+
connection.use_ssl = true
|
401
|
+
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
402
|
+
end
|
403
|
+
connection.open_timeout = 10
|
404
|
+
connection.read_timeout = 10
|
405
|
+
connection
|
406
|
+
end
|
407
|
+
|
408
|
+
# Run an HTTP operation
|
409
|
+
#
|
410
|
+
# @param [URI::HTTP] The URI to which to connect
|
411
|
+
# @param [Net::HTTPRequest] The request to make
|
412
|
+
# @return [Net::HTTPResponse]
|
413
|
+
def http(url, request)
|
414
|
+
request['User-Agent'] = USER_AGENT
|
415
|
+
|
416
|
+
http_connection(url).start do |http|
|
417
|
+
http.request request
|
418
|
+
end
|
419
|
+
rescue Timeout::Error
|
420
|
+
raise "Could not connect to #{api_url}"
|
421
|
+
end
|
422
|
+
|
423
|
+
# Called after an HTTP response to gist to perform post-processing.
|
424
|
+
#
|
425
|
+
# @param [String] body the text body from the github api
|
426
|
+
# @param [Hash] options more detailed options, see
|
427
|
+
# the documentation for {multi_gist}
|
428
|
+
def on_success(body, options={})
|
429
|
+
json = JSON.parse(body)
|
430
|
+
|
431
|
+
output = case options[:output]
|
432
|
+
when :javascript
|
433
|
+
%Q{<script src="#{json['html_url']}.js"></script>}
|
434
|
+
when :html_url
|
435
|
+
json['html_url']
|
436
|
+
when :raw_url
|
437
|
+
rawify(json['html_url'])
|
438
|
+
when :short_url
|
439
|
+
shorten(json['html_url'])
|
440
|
+
when :short_raw_url
|
441
|
+
shorten(rawify(json['html_url']))
|
442
|
+
else
|
443
|
+
json
|
444
|
+
end
|
445
|
+
|
446
|
+
Gist.copy(output.to_s) if options[:copy]
|
447
|
+
Gist.open(json['html_url']) if options[:open]
|
448
|
+
|
449
|
+
output
|
450
|
+
end
|
451
|
+
|
452
|
+
# Copy a string to the clipboard.
|
453
|
+
#
|
454
|
+
# @param [String] content
|
455
|
+
# @raise [Gist::Error] if no clipboard integration could be found
|
456
|
+
#
|
457
|
+
def copy(content)
|
458
|
+
IO.popen(clipboard_command(:copy), 'r+') { |clip| clip.print content }
|
459
|
+
|
460
|
+
unless paste == content
|
461
|
+
message = 'Copying to clipboard failed.'
|
462
|
+
|
463
|
+
if ENV["TMUX"] && clipboard_command(:copy) == 'pbcopy'
|
464
|
+
message << "\nIf you're running tmux on a mac, try http://robots.thoughtbot.com/post/19398560514/how-to-copy-and-paste-with-tmux-on-mac-os-x"
|
465
|
+
end
|
466
|
+
|
467
|
+
raise Error, message
|
468
|
+
end
|
469
|
+
rescue Error => e
|
470
|
+
raise ClipboardError, e.message + "\nAttempted to copy: #{content}"
|
471
|
+
end
|
472
|
+
|
473
|
+
# Get a string from the clipboard.
|
474
|
+
#
|
475
|
+
# @param [String] content
|
476
|
+
# @raise [Gist::Error] if no clipboard integration could be found
|
477
|
+
def paste
|
478
|
+
`#{clipboard_command(:paste)}`
|
479
|
+
end
|
480
|
+
|
481
|
+
# Find command from PATH environment.
|
482
|
+
#
|
483
|
+
# @param [String] cmd command name to find
|
484
|
+
# @param [String] options PATH environment variable
|
485
|
+
# @return [String] the command found
|
486
|
+
def which(cmd, path=ENV['PATH'])
|
487
|
+
if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin|cygwin/
|
488
|
+
path.split(File::PATH_SEPARATOR).each {|dir|
|
489
|
+
f = File.join(dir, cmd+".exe")
|
490
|
+
return f if File.executable?(f) && !File.directory?(f)
|
491
|
+
}
|
492
|
+
nil
|
493
|
+
else
|
494
|
+
return system("which #{cmd} > /dev/null 2>&1")
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# Get the command to use for the clipboard action.
|
499
|
+
#
|
500
|
+
# @param [Symbol] action either :copy or :paste
|
501
|
+
# @return [String] the command to run
|
502
|
+
# @raise [Gist::ClipboardError] if no clipboard integration could be found
|
503
|
+
def clipboard_command(action)
|
504
|
+
command = CLIPBOARD_COMMANDS.keys.detect do |cmd|
|
505
|
+
which cmd
|
506
|
+
end
|
507
|
+
raise ClipboardError, <<-EOT unless command
|
508
|
+
Could not find copy command, tried:
|
509
|
+
#{CLIPBOARD_COMMANDS.values.join(' || ')}
|
510
|
+
EOT
|
511
|
+
action == :copy ? command : CLIPBOARD_COMMANDS[command]
|
512
|
+
end
|
513
|
+
|
514
|
+
# Open a URL in a browser.
|
515
|
+
#
|
516
|
+
# @param [String] url
|
517
|
+
# @raise [RuntimeError] if no browser integration could be found
|
518
|
+
#
|
519
|
+
# This method was heavily inspired by defunkt's Gist#open,
|
520
|
+
# @see https://github.com/defunkt/gist/blob/bca9b29/lib/gist.rb#L157
|
521
|
+
def open(url)
|
522
|
+
command = if ENV['BROWSER']
|
523
|
+
ENV['BROWSER']
|
524
|
+
elsif RUBY_PLATFORM =~ /darwin/
|
525
|
+
'open'
|
526
|
+
elsif RUBY_PLATFORM =~ /linux/
|
527
|
+
%w(
|
528
|
+
sensible-browser
|
529
|
+
xdg-open
|
530
|
+
firefox
|
531
|
+
firefox-bin
|
532
|
+
).detect do |cmd|
|
533
|
+
which cmd
|
534
|
+
end
|
535
|
+
elsif ENV['OS'] == 'Windows_NT' || RUBY_PLATFORM =~ /djgpp|(cyg|ms|bcc)win|mingw|wince/i
|
536
|
+
'start ""'
|
537
|
+
else
|
538
|
+
raise "Could not work out how to use a browser."
|
539
|
+
end
|
540
|
+
|
541
|
+
`#{command} #{url}`
|
542
|
+
end
|
543
|
+
|
544
|
+
# Get the API base path
|
545
|
+
def base_path
|
546
|
+
ENV.key?(URL_ENV_NAME) ? GHE_BASE_PATH : GITHUB_BASE_PATH
|
547
|
+
end
|
548
|
+
|
549
|
+
# Get the API URL
|
550
|
+
def api_url
|
551
|
+
ENV.key?(URL_ENV_NAME) ? URI(ENV[URL_ENV_NAME]) : GITHUB_API_URL
|
552
|
+
end
|
553
|
+
|
554
|
+
def legacy_private_gister?
|
555
|
+
return unless which('git')
|
556
|
+
`git config --global gist.private` =~ /\Ayes|1|true|on\z/i
|
557
|
+
end
|
558
|
+
|
559
|
+
def should_be_public?(options={})
|
560
|
+
if options.key? :private
|
561
|
+
!options[:private]
|
562
|
+
else
|
563
|
+
!Gist.legacy_private_gister?
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|